@fgv/ts-extras 5.1.0-33 → 5.1.0-35

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.
Files changed (108) hide show
  1. package/dist/packlets/ai-assist/apiClient.js +58 -112
  2. package/dist/packlets/ai-assist/apiClient.js.map +1 -1
  3. package/dist/packlets/ai-assist/chatRequestBuilders.js +131 -35
  4. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  5. package/dist/packlets/ai-assist/converters.js +31 -1
  6. package/dist/packlets/ai-assist/converters.js.map +1 -1
  7. package/dist/packlets/ai-assist/embeddingClient.js +346 -0
  8. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -0
  9. package/dist/packlets/ai-assist/http.js +75 -0
  10. package/dist/packlets/ai-assist/http.js.map +1 -0
  11. package/dist/packlets/ai-assist/index.js +6 -4
  12. package/dist/packlets/ai-assist/index.js.map +1 -1
  13. package/dist/packlets/ai-assist/jsonCompletion.js +6 -8
  14. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
  15. package/dist/packlets/ai-assist/model.js +36 -1
  16. package/dist/packlets/ai-assist/model.js.map +1 -1
  17. package/dist/packlets/ai-assist/registry.js +77 -7
  18. package/dist/packlets/ai-assist/registry.js.map +1 -1
  19. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +176 -32
  20. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  21. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +528 -0
  22. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
  23. package/dist/packlets/ai-assist/streamingAdapters/common.js +95 -0
  24. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  25. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +34 -10
  26. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  27. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +215 -15
  28. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +15 -8
  30. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  31. package/dist/packlets/ai-assist/streamingClient.js +29 -5
  32. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  33. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +23 -0
  34. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  35. package/dist/packlets/ai-assist/toolFormats.js +106 -10
  36. package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
  37. package/dist/ts-extras.d.ts +682 -48
  38. package/lib/packlets/ai-assist/apiClient.d.ts +24 -34
  39. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
  40. package/lib/packlets/ai-assist/apiClient.js +67 -121
  41. package/lib/packlets/ai-assist/apiClient.js.map +1 -1
  42. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +82 -22
  43. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  44. package/lib/packlets/ai-assist/chatRequestBuilders.js +132 -34
  45. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  46. package/lib/packlets/ai-assist/converters.d.ts +9 -1
  47. package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
  48. package/lib/packlets/ai-assist/converters.js +31 -1
  49. package/lib/packlets/ai-assist/converters.js.map +1 -1
  50. package/lib/packlets/ai-assist/embeddingClient.d.ts +69 -0
  51. package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -0
  52. package/lib/packlets/ai-assist/embeddingClient.js +350 -0
  53. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -0
  54. package/lib/packlets/ai-assist/http.d.ts +24 -0
  55. package/lib/packlets/ai-assist/http.d.ts.map +1 -0
  56. package/lib/packlets/ai-assist/http.js +78 -0
  57. package/lib/packlets/ai-assist/http.js.map +1 -0
  58. package/lib/packlets/ai-assist/index.d.ts +6 -4
  59. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  60. package/lib/packlets/ai-assist/index.js +11 -1
  61. package/lib/packlets/ai-assist/index.js.map +1 -1
  62. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/jsonCompletion.js +6 -8
  64. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
  65. package/lib/packlets/ai-assist/model.d.ts +377 -5
  66. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/model.js +37 -2
  68. package/lib/packlets/ai-assist/model.js.map +1 -1
  69. package/lib/packlets/ai-assist/registry.d.ts +23 -1
  70. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  71. package/lib/packlets/ai-assist/registry.js +79 -7
  72. package/lib/packlets/ai-assist/registry.js.map +1 -1
  73. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +58 -5
  74. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  75. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +175 -31
  76. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  77. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +172 -0
  78. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -0
  79. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +534 -0
  80. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
  81. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +59 -11
  82. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  83. package/lib/packlets/ai-assist/streamingAdapters/common.js +97 -0
  84. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  85. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +16 -2
  86. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  87. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +34 -10
  88. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  89. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +15 -2
  90. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  91. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +214 -14
  92. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  93. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
  94. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +14 -7
  95. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  96. package/lib/packlets/ai-assist/streamingClient.d.ts +17 -0
  97. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  98. package/lib/packlets/ai-assist/streamingClient.js +31 -6
  99. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  100. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +18 -2
  101. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
  102. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +24 -0
  103. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  104. package/lib/packlets/ai-assist/toolFormats.d.ts +40 -9
  105. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
  106. package/lib/packlets/ai-assist/toolFormats.js +107 -10
  107. package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
  108. package/package.json +7 -7
@@ -5,35 +5,95 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
+ import { type JsonObject } from '@fgv/ts-json-base';
9
+ import { Result } from '@fgv/ts-utils';
8
10
  import { AiPrompt, type IChatMessage } from './model';
9
11
  /**
10
- * Optional head/tail messages to weave around the prompt's user message.
12
+ * The result of splitting an {@link AiAssist.IChatRequest} into the per-builder
13
+ * inputs: the current turn (as an {@link AiPrompt}, carrying system + the final
14
+ * user message + any attachments) and the preceding conversation history.
15
+ * @internal
16
+ */
17
+ export interface ISplitChatRequest {
18
+ /** The current turn lowered to an {@link AiPrompt} (system + user + attachments). */
19
+ readonly prompt: AiPrompt;
20
+ /** Prior conversation history — every message before the current turn. */
21
+ readonly head: ReadonlyArray<IChatMessage>;
22
+ }
23
+ /**
24
+ * Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the
25
+ * per-provider builders. The **last** message is the current `user` turn; the
26
+ * preceding messages are history (`head`). This is the single linearization
27
+ * shared by every turn entry point, so the completion path and the client-tool
28
+ * turn path place history at the identical position relative to the current turn.
29
+ *
30
+ * Fails when `messages` is empty (no current turn) or when the last message is
31
+ * not a `user` turn — relabelling a trailing assistant message as the user turn
32
+ * would be a silent footgun, so it is rejected loudly instead.
33
+ *
34
+ * @internal
35
+ */
36
+ export declare function splitChatRequest(system: string | undefined, messages: ReadonlyArray<IChatMessage>): Result<ISplitChatRequest>;
37
+ /**
38
+ * Rebuilds the ordered messages for a proxy wire body so that only the current
39
+ * turn can carry attachments — history (non-current) messages are reduced to
40
+ * `{ role, content }`. The direct per-provider builders already drop attachments
41
+ * on history turns (only the current user turn's attachments are honored), so
42
+ * normalizing here keeps the proxy wire shape consistent with the direct paths
43
+ * and avoids transmitting attachment payloads the upstream provider would ignore.
44
+ *
45
+ * @internal
46
+ */
47
+ export declare function normalizeOutboundMessages(split: ISplitChatRequest): IChatMessage[];
48
+ /**
49
+ * Optional history (`head`) and raw continuation (`rawTail`) messages to weave
50
+ * around the prompt's current user message.
11
51
  *
12
52
  * @internal
13
53
  */
14
54
  export interface IBuildMessagesOptions {
15
55
  /**
16
- * Messages inserted between the system prompt and the prompt's user
17
- * message (e.g. prior conversation history for multi-turn chat).
56
+ * Prior conversation history inserted between the system prompt and the
57
+ * prompt's current user message (multi-turn chat / correction retries). The
58
+ * single ordered linearization is `[system, ...head, user, ...rawTail]`.
18
59
  */
19
60
  readonly head?: ReadonlyArray<IChatMessage>;
20
61
  /**
21
- * Messages appended after the prompt's user message (e.g. assistant
22
- * + correction turns for the JSON-validation retry loop).
62
+ * Raw JSON objects appended after the prompt's user message. Used to
63
+ * inject provider-specific continuation messages (e.g. Anthropic assistant
64
+ * turns with thinking blocks, OpenAI Responses `function_call` /
65
+ * `function_call_output` items, Gemini `functionCall` / `functionResponse`
66
+ * turns) that cannot be expressed as plain {@link IChatMessage} objects.
67
+ *
68
+ * Each builder applies its own provider-specific shape guard:
69
+ * - {@link buildAnthropicMessages} projects each entry to `{ role, content }`.
70
+ * - {@link buildMessages} (OpenAI / xAI Responses) preserves each item
71
+ * verbatim (item fields differ per `type`), guarding only that it is a
72
+ * JSON object.
73
+ * - {@link buildGeminiContents} projects each entry to `{ role, parts }`.
74
+ *
75
+ * Entries that fail their builder's shape check are silently skipped (the
76
+ * caller is responsible for supplying well-formed continuation messages).
77
+ * Appended after the current user message.
23
78
  */
24
- readonly tail?: ReadonlyArray<IChatMessage>;
79
+ readonly rawTail?: ReadonlyArray<JsonObject>;
25
80
  }
26
81
  /**
27
- * Builds the messages array from prompt + optional head/tail messages.
28
- * The caller supplies the user content (string for text-only, parts array
29
- * for vision prompts) since the parts shape differs by format.
82
+ * Builds the messages array from prompt + optional history (`head`) and raw
83
+ * continuation (`rawTail`) messages. The caller supplies the user content
84
+ * (string for text-only, parts array for vision prompts) since the parts shape
85
+ * differs by format.
86
+ *
87
+ * `rawTail` items (OpenAI / xAI Responses `function_call` /
88
+ * `function_call_output` continuation items) are appended verbatim after the
89
+ * user message — their fields differ per item `type`, so they are preserved
90
+ * rather than projected. The return type is `Array<Record<string, unknown>>`
91
+ * to accommodate both `{ role, content }` messages and these heterogeneous
92
+ * input items.
30
93
  *
31
94
  * @internal
32
95
  */
33
- export declare function buildMessages(systemPrompt: string, userContent: string | unknown[], options?: IBuildMessagesOptions): Array<{
34
- role: string;
35
- content: string | unknown[];
36
- }>;
96
+ export declare function buildMessages(systemPrompt: string, userContent: string | unknown[], options?: IBuildMessagesOptions): Array<Record<string, unknown>>;
37
97
  /**
38
98
  * Builds the user content for OpenAI Chat Completions when attachments are
39
99
  * present. Returns a string when there are no attachments.
@@ -63,10 +123,10 @@ export declare function buildAnthropicUserContent(prompt: AiPrompt): string | un
63
123
  */
64
124
  export declare function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>>;
65
125
  /**
66
- * Builds the Anthropic messages array, weaving any `head` messages between
67
- * implicit system + the prompt's user message and appending `tail` messages
68
- * after. System messages are filtered out (Anthropic uses a top-level system
69
- * field).
126
+ * Builds the Anthropic messages array, weaving any `head` history messages
127
+ * between implicit system + the prompt's user message and appending `rawTail`
128
+ * continuation messages after. System messages are filtered out (Anthropic uses
129
+ * a top-level system field).
70
130
  *
71
131
  * @internal
72
132
  */
@@ -75,15 +135,15 @@ export declare function buildAnthropicMessages(prompt: AiPrompt, options?: IBuil
75
135
  content: string | unknown[];
76
136
  }>;
77
137
  /**
78
- * Builds the Gemini `contents` array, weaving any `head` messages before the
79
- * prompt's user parts and appending `tail` messages after. System messages
80
- * are filtered out (Gemini uses a top-level systemInstruction field) and
81
- * assistant roles are mapped to Gemini's `model` role.
138
+ * Builds the Gemini `contents` array, weaving any `head` history messages before
139
+ * the prompt's user parts and appending `rawTail` continuation messages after.
140
+ * System messages are filtered out (Gemini uses a top-level systemInstruction
141
+ * field) and assistant roles are mapped to Gemini's `model` role.
82
142
  *
83
143
  * @internal
84
144
  */
85
145
  export declare function buildGeminiContents(prompt: AiPrompt, options?: IBuildMessagesOptions): Array<{
86
146
  role: string;
87
- parts: Array<Record<string, unknown>>;
147
+ parts: unknown[];
88
148
  }>;
89
149
  //# sourceMappingURL=chatRequestBuilders.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chatRequestBuilders.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAA2B,KAAK,YAAY,EAAa,MAAM,SAAS,CAAC;AAE1F;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAAG,OAAO,EAAE,EAC/B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE,CAAA;CAAE,CAAC,CAkBtD;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAc/E;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAYpF;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAe9E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMrF;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE,CAAA;CAAE,CAAC,CAoBtD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAAE,CAAC,CA0BhE"}
1
+ {"version":3,"file":"chatRequestBuilders.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AAEH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAoC,MAAM,EAAW,MAAM,eAAe,CAAC;AAElF,OAAO,EAAE,QAAQ,EAA2B,KAAK,YAAY,EAAa,MAAM,SAAS,CAAC;AAE1F;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GACpC,MAAM,CAAC,iBAAiB,CAAC,CAW3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,EAAE,CAKlF;AAgED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CAC9C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAAG,OAAO,EAAE,EAC/B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAoBhC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAc/E;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAYpF;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,EAAE,CAe9E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMrF;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE,CAAA;CAAE,CAAC,CAmBtD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAwB3C"}
@@ -19,6 +19,8 @@
19
19
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
20
  // SOFTWARE.
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.splitChatRequest = splitChatRequest;
23
+ exports.normalizeOutboundMessages = normalizeOutboundMessages;
22
24
  exports.buildMessages = buildMessages;
23
25
  exports.buildOpenAiChatUserContent = buildOpenAiChatUserContent;
24
26
  exports.buildOpenAiResponsesUserContent = buildOpenAiResponsesUserContent;
@@ -33,29 +35,128 @@ exports.buildGeminiContents = buildGeminiContents;
33
35
  *
34
36
  * @packageDocumentation
35
37
  */
38
+ const ts_json_base_1 = require("@fgv/ts-json-base");
39
+ const ts_utils_1 = require("@fgv/ts-utils");
36
40
  const model_1 = require("./model");
37
41
  /**
38
- * Builds the messages array from prompt + optional head/tail messages.
39
- * The caller supplies the user content (string for text-only, parts array
40
- * for vision prompts) since the parts shape differs by format.
42
+ * Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the
43
+ * per-provider builders. The **last** message is the current `user` turn; the
44
+ * preceding messages are history (`head`). This is the single linearization
45
+ * shared by every turn entry point, so the completion path and the client-tool
46
+ * turn path place history at the identical position relative to the current turn.
47
+ *
48
+ * Fails when `messages` is empty (no current turn) or when the last message is
49
+ * not a `user` turn — relabelling a trailing assistant message as the user turn
50
+ * would be a silent footgun, so it is rejected loudly instead.
41
51
  *
42
52
  * @internal
43
53
  */
44
- function buildMessages(systemPrompt, userContent, options) {
45
- const messages = [
46
- { role: 'system', content: systemPrompt }
54
+ function splitChatRequest(system, messages) {
55
+ if (messages.length === 0) {
56
+ return (0, ts_utils_1.fail)('messages must contain at least one entry (the current user turn)');
57
+ }
58
+ const current = messages[messages.length - 1];
59
+ if (current.role !== 'user') {
60
+ return (0, ts_utils_1.fail)(`the last message must be the current user turn (role 'user'); got '${current.role}'`);
61
+ }
62
+ const head = messages.slice(0, messages.length - 1);
63
+ const prompt = new model_1.AiPrompt(current.content, system !== null && system !== void 0 ? system : '', current.attachments);
64
+ return (0, ts_utils_1.succeed)({ prompt, head });
65
+ }
66
+ /**
67
+ * Rebuilds the ordered messages for a proxy wire body so that only the current
68
+ * turn can carry attachments — history (non-current) messages are reduced to
69
+ * `{ role, content }`. The direct per-provider builders already drop attachments
70
+ * on history turns (only the current user turn's attachments are honored), so
71
+ * normalizing here keeps the proxy wire shape consistent with the direct paths
72
+ * and avoids transmitting attachment payloads the upstream provider would ignore.
73
+ *
74
+ * @internal
75
+ */
76
+ function normalizeOutboundMessages(split) {
77
+ return [
78
+ ...split.head.map((m) => ({ role: m.role, content: m.content })),
79
+ ...split.prompt.toRequest().messages
47
80
  ];
48
- /* c8 ignore next 4 - head branch: options?.head short-circuit not reached from current call sites */
81
+ }
82
+ /**
83
+ * Converter for a rawTail message entry. Narrows a `JsonObject` to
84
+ * `{ role: string; content: string | unknown[] }` at runtime using the
85
+ * Converter pattern. Entries that fail validation are silently skipped — the
86
+ * surrounding function is infallible, and a malformed continuation message is
87
+ * better omitted than transmitted verbatim.
88
+ * @internal
89
+ */
90
+ const rawTailMessageConverter = ts_utils_1.Converters.object({
91
+ role: ts_utils_1.Converters.enumeratedValue(['user', 'assistant']),
92
+ content: ts_utils_1.Converters.oneOf([
93
+ ts_utils_1.Converters.string,
94
+ ts_utils_1.Converters.isA('array', (v) => Array.isArray(v))
95
+ ])
96
+ }, { strict: false });
97
+ /**
98
+ * Converter for an OpenAI / xAI Responses API `rawTail` item. These are
99
+ * provider-native input items (`function_call`, `function_call_output`) whose
100
+ * fields differ per item type, so — unlike the Anthropic `{ role, content }`
101
+ * projection — the whole object is preserved verbatim.
102
+ *
103
+ * The static input is already typed `JsonObject`, so the `isJsonObject` guard
104
+ * is a runtime backstop, not a compile-time narrowing: continuation messages
105
+ * originate from a prior turn's `IAiClientToolContinuation.messages` and a
106
+ * consumer may persist and reload them through untyped JSON before passing them
107
+ * back. The guard preserves the same "a malformed continuation message is
108
+ * better omitted than transmitted verbatim" posture as the Anthropic path —
109
+ * non-object entries fail conversion and are skipped by the caller.
110
+ * @internal
111
+ */
112
+ const openAiRawTailItemConverter = ts_utils_1.Converters.isA('JsonObject', (v) => (0, ts_json_base_1.isJsonObject)(v));
113
+ /**
114
+ * Converter for a Gemini `rawTail` item. Gemini continuation messages are
115
+ * `{ role, parts }` turns (a model turn with `functionCall` parts followed by a
116
+ * user turn with `functionResponse` parts). Narrows a `JsonObject` to
117
+ * `{ role: 'user' | 'model'; parts: Array<Record<string, unknown>> }`; entries
118
+ * that fail validation are skipped by the caller.
119
+ * @internal
120
+ */
121
+ const geminiRawTailMessageConverter = ts_utils_1.Converters.object({
122
+ role: ts_utils_1.Converters.enumeratedValue(['user', 'model']),
123
+ // `parts` is preserved verbatim and serialized into the request body, so the
124
+ // element shape is not narrowed here — `Array.isArray` soundly guarantees
125
+ // `unknown[]` (narrowing to `Record<string, unknown>[]` would be an unchecked cast).
126
+ parts: ts_utils_1.Converters.isA('array', (v) => Array.isArray(v))
127
+ }, { strict: false });
128
+ /**
129
+ * Builds the messages array from prompt + optional history (`head`) and raw
130
+ * continuation (`rawTail`) messages. The caller supplies the user content
131
+ * (string for text-only, parts array for vision prompts) since the parts shape
132
+ * differs by format.
133
+ *
134
+ * `rawTail` items (OpenAI / xAI Responses `function_call` /
135
+ * `function_call_output` continuation items) are appended verbatim after the
136
+ * user message — their fields differ per item `type`, so they are preserved
137
+ * rather than projected. The return type is `Array<Record<string, unknown>>`
138
+ * to accommodate both `{ role, content }` messages and these heterogeneous
139
+ * input items.
140
+ *
141
+ * @internal
142
+ */
143
+ function buildMessages(systemPrompt, userContent, options) {
144
+ const messages = [{ role: 'system', content: systemPrompt }];
49
145
  if (options === null || options === void 0 ? void 0 : options.head) {
50
146
  for (const msg of options.head) {
51
147
  messages.push({ role: msg.role, content: msg.content });
52
148
  }
53
149
  }
54
150
  messages.push({ role: 'user', content: userContent });
55
- /* c8 ignore next 4 - tail branch: options?.tail short-circuit not reached from current call sites */
56
- if (options === null || options === void 0 ? void 0 : options.tail) {
57
- for (const msg of options.tail) {
58
- messages.push({ role: msg.role, content: msg.content });
151
+ // OpenAI / xAI Responses continuation items (function_call /
152
+ // function_call_output) are appended verbatim their field set differs per
153
+ // item type, so the whole object is preserved rather than projected.
154
+ if (options === null || options === void 0 ? void 0 : options.rawTail) {
155
+ for (const item of options.rawTail) {
156
+ const converted = openAiRawTailItemConverter.convert(item);
157
+ if (converted.isSuccess()) {
158
+ messages.push(converted.value);
159
+ }
59
160
  }
60
161
  }
61
162
  return messages;
@@ -129,16 +230,15 @@ function buildGeminiUserParts(prompt) {
129
230
  return parts;
130
231
  }
131
232
  /**
132
- * Builds the Anthropic messages array, weaving any `head` messages between
133
- * implicit system + the prompt's user message and appending `tail` messages
134
- * after. System messages are filtered out (Anthropic uses a top-level system
135
- * field).
233
+ * Builds the Anthropic messages array, weaving any `head` history messages
234
+ * between implicit system + the prompt's user message and appending `rawTail`
235
+ * continuation messages after. System messages are filtered out (Anthropic uses
236
+ * a top-level system field).
136
237
  *
137
238
  * @internal
138
239
  */
139
240
  function buildAnthropicMessages(prompt, options) {
140
241
  const messages = [];
141
- /* c8 ignore next 5 - head branch: options?.head short-circuit not reached from current call sites */
142
242
  if (options === null || options === void 0 ? void 0 : options.head) {
143
243
  for (const msg of options.head) {
144
244
  if (msg.role !== 'system') {
@@ -147,27 +247,26 @@ function buildAnthropicMessages(prompt, options) {
147
247
  }
148
248
  }
149
249
  messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });
150
- /* c8 ignore next 5 - tail branch: options?.tail short-circuit not reached from current call sites */
151
- if (options === null || options === void 0 ? void 0 : options.tail) {
152
- for (const msg of options.tail) {
153
- if (msg.role !== 'system') {
154
- messages.push({ role: msg.role, content: msg.content });
250
+ if (options === null || options === void 0 ? void 0 : options.rawTail) {
251
+ for (const msg of options.rawTail) {
252
+ const converted = rawTailMessageConverter.convert(msg);
253
+ if (converted.isSuccess()) {
254
+ messages.push(converted.value);
155
255
  }
156
256
  }
157
257
  }
158
258
  return messages;
159
259
  }
160
260
  /**
161
- * Builds the Gemini `contents` array, weaving any `head` messages before the
162
- * prompt's user parts and appending `tail` messages after. System messages
163
- * are filtered out (Gemini uses a top-level systemInstruction field) and
164
- * assistant roles are mapped to Gemini's `model` role.
261
+ * Builds the Gemini `contents` array, weaving any `head` history messages before
262
+ * the prompt's user parts and appending `rawTail` continuation messages after.
263
+ * System messages are filtered out (Gemini uses a top-level systemInstruction
264
+ * field) and assistant roles are mapped to Gemini's `model` role.
165
265
  *
166
266
  * @internal
167
267
  */
168
268
  function buildGeminiContents(prompt, options) {
169
269
  const contents = [];
170
- /* c8 ignore next 7 - head branch: options?.head short-circuit not reached from current call sites */
171
270
  if (options === null || options === void 0 ? void 0 : options.head) {
172
271
  for (const msg of options.head) {
173
272
  if (msg.role !== 'system') {
@@ -179,14 +278,13 @@ function buildGeminiContents(prompt, options) {
179
278
  }
180
279
  }
181
280
  contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });
182
- /* c8 ignore next 7 - tail branch: options?.tail short-circuit not reached from current call sites */
183
- if (options === null || options === void 0 ? void 0 : options.tail) {
184
- for (const msg of options.tail) {
185
- if (msg.role !== 'system') {
186
- contents.push({
187
- role: msg.role === 'assistant' ? 'model' : msg.role,
188
- parts: [{ text: msg.content }]
189
- });
281
+ // Gemini continuation turns (model `functionCall` parts + user
282
+ // `functionResponse` parts) are projected to `{ role, parts }`.
283
+ if (options === null || options === void 0 ? void 0 : options.rawTail) {
284
+ for (const item of options.rawTail) {
285
+ const converted = geminiRawTailMessageConverter.convert(item);
286
+ if (converted.isSuccess()) {
287
+ contents.push(converted.value);
190
288
  }
191
289
  }
192
290
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chatRequestBuilders.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Per-format chat request shape builders. Shared between the synchronous\n * (`apiClient.ts`) and streaming (`streamingClient.ts`) paths so the wire\n * shapes stay consistent.\n *\n * @packageDocumentation\n */\n\nimport { AiPrompt, type IAiImageAttachment, type IChatMessage, toDataUrl } from './model';\n\n/**\n * Optional head/tail messages to weave around the prompt's user message.\n *\n * @internal\n */\nexport interface IBuildMessagesOptions {\n /**\n * Messages inserted between the system prompt and the prompt's user\n * message (e.g. prior conversation history for multi-turn chat).\n */\n readonly head?: ReadonlyArray<IChatMessage>;\n /**\n * Messages appended after the prompt's user message (e.g. assistant\n * + correction turns for the JSON-validation retry loop).\n */\n readonly tail?: ReadonlyArray<IChatMessage>;\n}\n\n/**\n * Builds the messages array from prompt + optional head/tail messages.\n * The caller supplies the user content (string for text-only, parts array\n * for vision prompts) since the parts shape differs by format.\n *\n * @internal\n */\nexport function buildMessages(\n systemPrompt: string,\n userContent: string | unknown[],\n options?: IBuildMessagesOptions\n): Array<{ role: string; content: string | unknown[] }> {\n const messages: Array<{ role: string; content: string | unknown[] }> = [\n { role: 'system', content: systemPrompt }\n ];\n /* c8 ignore next 4 - head branch: options?.head short-circuit not reached from current call sites */\n if (options?.head) {\n for (const msg of options.head) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n messages.push({ role: 'user', content: userContent });\n /* c8 ignore next 4 - tail branch: options?.tail short-circuit not reached from current call sites */\n if (options?.tail) {\n for (const msg of options.tail) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n return messages;\n}\n\n/**\n * Builds the user content for OpenAI Chat Completions when attachments are\n * present. Returns a string when there are no attachments.\n *\n * @internal\n */\nexport function buildOpenAiChatUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image_url',\n image_url: {\n url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }\n }))\n ];\n}\n\n/**\n * Builds the user content for OpenAI / xAI Responses API when attachments\n * are present. Responses API uses `input_text` / `input_image` part types,\n * distinct from Chat Completions' `text` / `image_url`.\n *\n * @internal\n */\nexport function buildOpenAiResponsesUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'input_text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'input_image',\n image_url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }))\n ];\n}\n\n/**\n * Builds the user-message content for Anthropic when attachments are present.\n *\n * @internal\n */\nexport function buildAnthropicUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image',\n source: {\n type: 'base64',\n media_type: att.mimeType,\n data: att.base64\n }\n }))\n ];\n}\n\n/**\n * Builds the Gemini `parts` array for the user turn, including any image\n * attachments as `inlineData` parts.\n *\n * @internal\n */\nexport function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>> {\n const parts: Array<Record<string, unknown>> = [{ text: prompt.user }];\n for (const att of prompt.attachments) {\n parts.push({ inlineData: { mimeType: att.mimeType, data: att.base64 } });\n }\n return parts;\n}\n\n/**\n * Builds the Anthropic messages array, weaving any `head` messages between\n * implicit system + the prompt's user message and appending `tail` messages\n * after. System messages are filtered out (Anthropic uses a top-level system\n * field).\n *\n * @internal\n */\nexport function buildAnthropicMessages(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; content: string | unknown[] }> {\n const messages: Array<{ role: string; content: string | unknown[] }> = [];\n /* c8 ignore next 5 - head branch: options?.head short-circuit not reached from current call sites */\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });\n /* c8 ignore next 5 - tail branch: options?.tail short-circuit not reached from current call sites */\n if (options?.tail) {\n for (const msg of options.tail) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the Gemini `contents` array, weaving any `head` messages before the\n * prompt's user parts and appending `tail` messages after. System messages\n * are filtered out (Gemini uses a top-level systemInstruction field) and\n * assistant roles are mapped to Gemini's `model` role.\n *\n * @internal\n */\nexport function buildGeminiContents(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; parts: Array<Record<string, unknown>> }> {\n const contents: Array<{ role: string; parts: Array<Record<string, unknown>> }> = [];\n /* c8 ignore next 7 - head branch: options?.head short-circuit not reached from current call sites */\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });\n /* c8 ignore next 7 - tail branch: options?.tail short-circuit not reached from current call sites */\n if (options?.tail) {\n for (const msg of options.tail) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n return contents;\n}\n"]}
1
+ {"version":3,"file":"chatRequestBuilders.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/chatRequestBuilders.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Per-format chat request shape builders. Shared between the synchronous\n * (`apiClient.ts`) and streaming (`streamingClient.ts`) paths so the wire\n * shapes stay consistent.\n *\n * @packageDocumentation\n */\n\nimport { isJsonObject, type JsonObject } from '@fgv/ts-json-base';\nimport { type Converter, Converters, fail, Result, succeed } from '@fgv/ts-utils';\n\nimport { AiPrompt, type IAiImageAttachment, type IChatMessage, toDataUrl } from './model';\n\n/**\n * The result of splitting an {@link AiAssist.IChatRequest} into the per-builder\n * inputs: the current turn (as an {@link AiPrompt}, carrying system + the final\n * user message + any attachments) and the preceding conversation history.\n * @internal\n */\nexport interface ISplitChatRequest {\n /** The current turn lowered to an {@link AiPrompt} (system + user + attachments). */\n readonly prompt: AiPrompt;\n /** Prior conversation history — every message before the current turn. */\n readonly head: ReadonlyArray<IChatMessage>;\n}\n\n/**\n * Splits a unified {@link AiAssist.IChatRequest} into `{ prompt, head }` for the\n * per-provider builders. The **last** message is the current `user` turn; the\n * preceding messages are history (`head`). This is the single linearization\n * shared by every turn entry point, so the completion path and the client-tool\n * turn path place history at the identical position relative to the current turn.\n *\n * Fails when `messages` is empty (no current turn) or when the last message is\n * not a `user` turn — relabelling a trailing assistant message as the user turn\n * would be a silent footgun, so it is rejected loudly instead.\n *\n * @internal\n */\nexport function splitChatRequest(\n system: string | undefined,\n messages: ReadonlyArray<IChatMessage>\n): Result<ISplitChatRequest> {\n if (messages.length === 0) {\n return fail('messages must contain at least one entry (the current user turn)');\n }\n const current = messages[messages.length - 1];\n if (current.role !== 'user') {\n return fail(`the last message must be the current user turn (role 'user'); got '${current.role}'`);\n }\n const head = messages.slice(0, messages.length - 1);\n const prompt = new AiPrompt(current.content, system ?? '', current.attachments);\n return succeed({ prompt, head });\n}\n\n/**\n * Rebuilds the ordered messages for a proxy wire body so that only the current\n * turn can carry attachments — history (non-current) messages are reduced to\n * `{ role, content }`. The direct per-provider builders already drop attachments\n * on history turns (only the current user turn's attachments are honored), so\n * normalizing here keeps the proxy wire shape consistent with the direct paths\n * and avoids transmitting attachment payloads the upstream provider would ignore.\n *\n * @internal\n */\nexport function normalizeOutboundMessages(split: ISplitChatRequest): IChatMessage[] {\n return [\n ...split.head.map((m) => ({ role: m.role, content: m.content })),\n ...split.prompt.toRequest().messages\n ];\n}\n\n/**\n * Converter for a rawTail message entry. Narrows a `JsonObject` to\n * `{ role: string; content: string | unknown[] }` at runtime using the\n * Converter pattern. Entries that fail validation are silently skipped — the\n * surrounding function is infallible, and a malformed continuation message is\n * better omitted than transmitted verbatim.\n * @internal\n */\nconst rawTailMessageConverter: Converter<{ role: 'user' | 'assistant'; content: string | unknown[] }> =\n Converters.object<{ role: 'user' | 'assistant'; content: string | unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'assistant'>(['user', 'assistant']),\n content: Converters.oneOf<string | unknown[]>([\n Converters.string,\n Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n ])\n },\n { strict: false }\n );\n\n/**\n * Converter for an OpenAI / xAI Responses API `rawTail` item. These are\n * provider-native input items (`function_call`, `function_call_output`) whose\n * fields differ per item type, so — unlike the Anthropic `{ role, content }`\n * projection — the whole object is preserved verbatim.\n *\n * The static input is already typed `JsonObject`, so the `isJsonObject` guard\n * is a runtime backstop, not a compile-time narrowing: continuation messages\n * originate from a prior turn's `IAiClientToolContinuation.messages` and a\n * consumer may persist and reload them through untyped JSON before passing them\n * back. The guard preserves the same \"a malformed continuation message is\n * better omitted than transmitted verbatim\" posture as the Anthropic path —\n * non-object entries fail conversion and are skipped by the caller.\n * @internal\n */\nconst openAiRawTailItemConverter: Converter<JsonObject> = Converters.isA<JsonObject>(\n 'JsonObject',\n (v): v is JsonObject => isJsonObject(v)\n);\n\n/**\n * Converter for a Gemini `rawTail` item. Gemini continuation messages are\n * `{ role, parts }` turns (a model turn with `functionCall` parts followed by a\n * user turn with `functionResponse` parts). Narrows a `JsonObject` to\n * `{ role: 'user' | 'model'; parts: Array<Record<string, unknown>> }`; entries\n * that fail validation are skipped by the caller.\n * @internal\n */\nconst geminiRawTailMessageConverter: Converter<{\n role: 'user' | 'model';\n parts: unknown[];\n}> = Converters.object<{ role: 'user' | 'model'; parts: unknown[] }>(\n {\n role: Converters.enumeratedValue<'user' | 'model'>(['user', 'model']),\n // `parts` is preserved verbatim and serialized into the request body, so the\n // element shape is not narrowed here — `Array.isArray` soundly guarantees\n // `unknown[]` (narrowing to `Record<string, unknown>[]` would be an unchecked cast).\n parts: Converters.isA('array', (v): v is unknown[] => Array.isArray(v))\n },\n { strict: false }\n);\n\n/**\n * Optional history (`head`) and raw continuation (`rawTail`) messages to weave\n * around the prompt's current user message.\n *\n * @internal\n */\nexport interface IBuildMessagesOptions {\n /**\n * Prior conversation history inserted between the system prompt and the\n * prompt's current user message (multi-turn chat / correction retries). The\n * single ordered linearization is `[system, ...head, user, ...rawTail]`.\n */\n readonly head?: ReadonlyArray<IChatMessage>;\n /**\n * Raw JSON objects appended after the prompt's user message. Used to\n * inject provider-specific continuation messages (e.g. Anthropic assistant\n * turns with thinking blocks, OpenAI Responses `function_call` /\n * `function_call_output` items, Gemini `functionCall` / `functionResponse`\n * turns) that cannot be expressed as plain {@link IChatMessage} objects.\n *\n * Each builder applies its own provider-specific shape guard:\n * - {@link buildAnthropicMessages} projects each entry to `{ role, content }`.\n * - {@link buildMessages} (OpenAI / xAI Responses) preserves each item\n * verbatim (item fields differ per `type`), guarding only that it is a\n * JSON object.\n * - {@link buildGeminiContents} projects each entry to `{ role, parts }`.\n *\n * Entries that fail their builder's shape check are silently skipped (the\n * caller is responsible for supplying well-formed continuation messages).\n * Appended after the current user message.\n */\n readonly rawTail?: ReadonlyArray<JsonObject>;\n}\n\n/**\n * Builds the messages array from prompt + optional history (`head`) and raw\n * continuation (`rawTail`) messages. The caller supplies the user content\n * (string for text-only, parts array for vision prompts) since the parts shape\n * differs by format.\n *\n * `rawTail` items (OpenAI / xAI Responses `function_call` /\n * `function_call_output` continuation items) are appended verbatim after the\n * user message — their fields differ per item `type`, so they are preserved\n * rather than projected. The return type is `Array<Record<string, unknown>>`\n * to accommodate both `{ role, content }` messages and these heterogeneous\n * input items.\n *\n * @internal\n */\nexport function buildMessages(\n systemPrompt: string,\n userContent: string | unknown[],\n options?: IBuildMessagesOptions\n): Array<Record<string, unknown>> {\n const messages: Array<Record<string, unknown>> = [{ role: 'system', content: systemPrompt }];\n if (options?.head) {\n for (const msg of options.head) {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n messages.push({ role: 'user', content: userContent });\n // OpenAI / xAI Responses continuation items (function_call /\n // function_call_output) are appended verbatim — their field set differs per\n // item type, so the whole object is preserved rather than projected.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = openAiRawTailItemConverter.convert(item);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the user content for OpenAI Chat Completions when attachments are\n * present. Returns a string when there are no attachments.\n *\n * @internal\n */\nexport function buildOpenAiChatUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image_url',\n image_url: {\n url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }\n }))\n ];\n}\n\n/**\n * Builds the user content for OpenAI / xAI Responses API when attachments\n * are present. Responses API uses `input_text` / `input_image` part types,\n * distinct from Chat Completions' `text` / `image_url`.\n *\n * @internal\n */\nexport function buildOpenAiResponsesUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'input_text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'input_image',\n image_url: toDataUrl(att),\n ...(att.detail !== undefined ? { detail: att.detail } : {})\n }))\n ];\n}\n\n/**\n * Builds the user-message content for Anthropic when attachments are present.\n *\n * @internal\n */\nexport function buildAnthropicUserContent(prompt: AiPrompt): string | unknown[] {\n if (prompt.attachments.length === 0) {\n return prompt.user;\n }\n return [\n { type: 'text', text: prompt.user },\n ...prompt.attachments.map((att: IAiImageAttachment) => ({\n type: 'image',\n source: {\n type: 'base64',\n media_type: att.mimeType,\n data: att.base64\n }\n }))\n ];\n}\n\n/**\n * Builds the Gemini `parts` array for the user turn, including any image\n * attachments as `inlineData` parts.\n *\n * @internal\n */\nexport function buildGeminiUserParts(prompt: AiPrompt): Array<Record<string, unknown>> {\n const parts: Array<Record<string, unknown>> = [{ text: prompt.user }];\n for (const att of prompt.attachments) {\n parts.push({ inlineData: { mimeType: att.mimeType, data: att.base64 } });\n }\n return parts;\n}\n\n/**\n * Builds the Anthropic messages array, weaving any `head` history messages\n * between implicit system + the prompt's user message and appending `rawTail`\n * continuation messages after. System messages are filtered out (Anthropic uses\n * a top-level system field).\n *\n * @internal\n */\nexport function buildAnthropicMessages(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; content: string | unknown[] }> {\n const messages: Array<{ role: string; content: string | unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n messages.push({ role: msg.role, content: msg.content });\n }\n }\n }\n messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });\n if (options?.rawTail) {\n for (const msg of options.rawTail) {\n const converted = rawTailMessageConverter.convert(msg);\n if (converted.isSuccess()) {\n messages.push(converted.value);\n }\n }\n }\n return messages;\n}\n\n/**\n * Builds the Gemini `contents` array, weaving any `head` history messages before\n * the prompt's user parts and appending `rawTail` continuation messages after.\n * System messages are filtered out (Gemini uses a top-level systemInstruction\n * field) and assistant roles are mapped to Gemini's `model` role.\n *\n * @internal\n */\nexport function buildGeminiContents(\n prompt: AiPrompt,\n options?: IBuildMessagesOptions\n): Array<{ role: string; parts: unknown[] }> {\n const contents: Array<{ role: string; parts: unknown[] }> = [];\n if (options?.head) {\n for (const msg of options.head) {\n if (msg.role !== 'system') {\n contents.push({\n role: msg.role === 'assistant' ? 'model' : msg.role,\n parts: [{ text: msg.content }]\n });\n }\n }\n }\n contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });\n // Gemini continuation turns (model `functionCall` parts + user\n // `functionResponse` parts) are projected to `{ role, parts }`.\n if (options?.rawTail) {\n for (const item of options.rawTail) {\n const converted = geminiRawTailMessageConverter.convert(item);\n if (converted.isSuccess()) {\n contents.push(converted.value);\n }\n }\n }\n return contents;\n}\n"]}
@@ -3,7 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  import { type Converter } from '@fgv/ts-utils';
6
- import { type AiProviderId, type AiServerToolConfig, type AiServerToolType, type IAiAssistProviderConfig, type IAiAssistSettings, type IAiToolEnablement, type IAiWebSearchToolConfig, type ModelSpec, type ModelSpecKey } from './model';
6
+ import { type AiProviderId, type AiServerToolConfig, type AiServerToolType, type IAiAssistProviderConfig, type IAiAssistSettings, type IAiClientToolConfig, type IAiToolEnablement, type IAiWebSearchToolConfig, type ModelSpec, type ModelSpecKey } from './model';
7
7
  /**
8
8
  * Converter for {@link AiProviderId}.
9
9
  * @public
@@ -24,6 +24,14 @@ export declare const aiWebSearchToolConfig: Converter<IAiWebSearchToolConfig>;
24
24
  * @public
25
25
  */
26
26
  export declare const aiServerToolConfig: Converter<AiServerToolConfig>;
27
+ /**
28
+ * Converter for {@link AiAssist.IAiClientToolConfig}. Validates the wrapper shape: `type`,
29
+ * `name`, `description`, and the presence of a usable `parametersSchema`.
30
+ * Does not inspect the inner JSON Schema structure — `JsonSchema.object(...)` already
31
+ * guarantees the schema is valid.
32
+ * @public
33
+ */
34
+ export declare const aiClientToolConfig: Converter<IAiClientToolConfig>;
27
35
  /**
28
36
  * Converter for {@link IAiToolEnablement}.
29
37
  * @public
@@ -1 +1 @@
1
- {"version":3,"file":"converters.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/converters.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAc,MAAM,eAAe,CAAC;AAE3D,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,SAAS,EACd,KAAK,YAAY,EAElB,MAAM,SAAS,CAAC;AAOjB;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,CAAC,YAAY,CAA4D,CAAC;AAY9G;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,CAAC,gBAAgB,CACS,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,CAAC,sBAAsB,CAOhE,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,kBAAkB,CAGzD,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,CAAC,iBAAiB,CAIxD,CAAC;AAMH;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,CAAC,YAAY,CACW,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,CAAC,SAAS,CAS1C,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,CAAC,uBAAuB,CAOlE,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,CAAC,iBAAiB,CAKxD,CAAC"}
1
+ {"version":3,"file":"converters.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/converters.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AAEH,OAAO,EAAE,KAAK,SAAS,EAA0C,MAAM,eAAe,CAAC;AAGvF,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,SAAS,EACd,KAAK,YAAY,EAElB,MAAM,SAAS,CAAC;AAOjB;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,CAAC,YAAY,CAA4D,CAAC;AAY9G;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,CAAC,gBAAgB,CACS,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,CAAC,sBAAsB,CAOhE,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,kBAAkB,CAGzD,CAAC;AAyBL;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,mBAAmB,CAO5D,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,CAAC,iBAAiB,CAIxD,CAAC;AAMH;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,CAAC,YAAY,CACW,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,CAAC,SAAS,CAS1C,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,CAAC,uBAAuB,CAOlE,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,CAAC,iBAAiB,CAKxD,CAAC"}
@@ -19,7 +19,7 @@
19
19
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
20
  // SOFTWARE.
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.aiAssistSettings = exports.aiAssistProviderConfig = exports.modelSpec = exports.modelSpecKey = exports.aiToolEnablement = exports.aiServerToolConfig = exports.aiWebSearchToolConfig = exports.aiServerToolType = exports.aiProviderId = void 0;
22
+ exports.aiAssistSettings = exports.aiAssistProviderConfig = exports.modelSpec = exports.modelSpecKey = exports.aiToolEnablement = exports.aiClientToolConfig = exports.aiServerToolConfig = exports.aiWebSearchToolConfig = exports.aiServerToolType = exports.aiProviderId = void 0;
23
23
  /**
24
24
  * Converters for AI assist settings types.
25
25
  * @packageDocumentation
@@ -66,6 +66,36 @@ exports.aiWebSearchToolConfig = ts_utils_1.Converters.strictObject({
66
66
  exports.aiServerToolConfig = ts_utils_1.Converters.discriminatedObject('type', {
67
67
  web_search: exports.aiWebSearchToolConfig
68
68
  });
69
+ // ============================================================================
70
+ // Client-Defined Tool Converters
71
+ // ============================================================================
72
+ /**
73
+ * Validator for the `parametersSchema` field of a client tool config.
74
+ * Checks that the value is a non-null object exposing both `validate` and `toJson`
75
+ * as callable functions — the runtime presence check for {@link JsonSchema.ISchemaValidator}.
76
+ * Does not inspect the inner JSON Schema structure.
77
+ * @internal
78
+ */
79
+ const parametersSchemaValidator = ts_utils_1.Validators.isA('ISchemaValidator (must expose .validate() and .toJson())', (v) => v !== null &&
80
+ v !== undefined &&
81
+ typeof v === 'object' &&
82
+ typeof v.validate === 'function' &&
83
+ typeof v.toJson === 'function');
84
+ /**
85
+ * Converter for {@link AiAssist.IAiClientToolConfig}. Validates the wrapper shape: `type`,
86
+ * `name`, `description`, and the presence of a usable `parametersSchema`.
87
+ * Does not inspect the inner JSON Schema structure — `JsonSchema.object(...)` already
88
+ * guarantees the schema is valid.
89
+ * @public
90
+ */
91
+ exports.aiClientToolConfig = ts_utils_1.Converters.object({
92
+ type: ts_utils_1.Converters.enumeratedValue(['client_tool']),
93
+ name: ts_utils_1.Converters.string.withConstraint((s) => s.length > 0, {
94
+ description: 'name must be a non-empty string'
95
+ }),
96
+ description: ts_utils_1.Converters.string,
97
+ parametersSchema: parametersSchemaValidator
98
+ });
69
99
  /**
70
100
  * Converter for {@link IAiToolEnablement}.
71
101
  * @public
@@ -1 +1 @@
1
- {"version":3,"file":"converters.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/converters.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Converters for AI assist settings types.\n * @packageDocumentation\n */\n\nimport { type Converter, Converters } from '@fgv/ts-utils';\n\nimport {\n type AiProviderId,\n type AiServerToolConfig,\n type AiServerToolType,\n type IAiAssistProviderConfig,\n type IAiAssistSettings,\n type IAiToolEnablement,\n type IAiWebSearchToolConfig,\n type ModelSpec,\n type ModelSpecKey,\n allModelSpecKeys\n} from './model';\nimport { allProviderIds } from './registry';\n\n// ============================================================================\n// Provider ID\n// ============================================================================\n\n/**\n * Converter for {@link AiProviderId}.\n * @public\n */\nexport const aiProviderId: Converter<AiProviderId> = Converters.enumeratedValue<AiProviderId>(allProviderIds);\n\n// ============================================================================\n// Server-Side Tool Converters\n// ============================================================================\n\n/**\n * All known server-side tool type values.\n * @internal\n */\nconst allServerToolTypes: ReadonlyArray<AiServerToolType> = ['web_search'];\n\n/**\n * Converter for {@link AiServerToolType}.\n * @public\n */\nexport const aiServerToolType: Converter<AiServerToolType> =\n Converters.enumeratedValue<AiServerToolType>(allServerToolTypes);\n\n/**\n * Converter for {@link IAiWebSearchToolConfig}.\n * @public\n */\nexport const aiWebSearchToolConfig: Converter<IAiWebSearchToolConfig> =\n Converters.strictObject<IAiWebSearchToolConfig>({\n type: Converters.enumeratedValue<'web_search'>(['web_search']),\n allowedDomains: Converters.arrayOf(Converters.string).optional(),\n blockedDomains: Converters.arrayOf(Converters.string).optional(),\n maxUses: Converters.number.optional(),\n enableImageUnderstanding: Converters.boolean.optional()\n });\n\n/**\n * Converter for {@link AiServerToolConfig} (discriminated union on `type`).\n * @public\n */\nexport const aiServerToolConfig: Converter<AiServerToolConfig> =\n Converters.discriminatedObject<AiServerToolConfig>('type', {\n web_search: aiWebSearchToolConfig\n });\n\n/**\n * Converter for {@link IAiToolEnablement}.\n * @public\n */\nexport const aiToolEnablement: Converter<IAiToolEnablement> = Converters.strictObject<IAiToolEnablement>({\n type: aiServerToolType,\n enabled: Converters.boolean,\n config: aiServerToolConfig.optional()\n});\n\n// ============================================================================\n// Model Specification\n// ============================================================================\n\n/**\n * Converter for {@link ModelSpecKey}.\n * @public\n */\nexport const modelSpecKey: Converter<ModelSpecKey> =\n Converters.enumeratedValue<ModelSpecKey>(allModelSpecKeys);\n\n/**\n * Recursive converter for {@link ModelSpec}.\n * Accepts a string or an object whose values are themselves ModelSpec values,\n * with keys constrained to known {@link ModelSpecKey} values.\n * Uses the `self` parameter from `Converters.generic` for recursion.\n * @public\n */\nexport const modelSpec: Converter<ModelSpec> = Converters.generic<ModelSpec>(\n (from: unknown, self: Converter<ModelSpec>) => {\n return Converters.oneOf<ModelSpec>([\n Converters.string,\n Converters.recordOf(self, { keyConverter: modelSpecKey })\n ])\n .withFormattedError(() => 'expected model spec (string or object with keys: base, tools, image)')\n .convert(from);\n }\n);\n\n// ============================================================================\n// Provider Config & Settings\n// ============================================================================\n\n/**\n * Converter for {@link IAiAssistProviderConfig}.\n * @public\n */\nexport const aiAssistProviderConfig: Converter<IAiAssistProviderConfig> =\n Converters.strictObject<IAiAssistProviderConfig>({\n provider: aiProviderId,\n secretName: Converters.string.optional(),\n model: modelSpec.optional(),\n tools: Converters.arrayOf(aiToolEnablement).optional(),\n endpoint: Converters.string.optional()\n });\n\n/**\n * Converter for {@link IAiAssistSettings}.\n * @public\n */\nexport const aiAssistSettings: Converter<IAiAssistSettings> = Converters.strictObject<IAiAssistSettings>({\n providers: Converters.arrayOf(aiAssistProviderConfig),\n defaultProvider: aiProviderId.optional(),\n proxyUrl: Converters.string.optional(),\n proxyAllProviders: Converters.boolean.optional()\n});\n"]}
1
+ {"version":3,"file":"converters.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/converters.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Converters for AI assist settings types.\n * @packageDocumentation\n */\n\nimport { type Converter, Converters, type Validator, Validators } from '@fgv/ts-utils';\nimport { type JsonSchema } from '@fgv/ts-json-base';\n\nimport {\n type AiProviderId,\n type AiServerToolConfig,\n type AiServerToolType,\n type IAiAssistProviderConfig,\n type IAiAssistSettings,\n type IAiClientToolConfig,\n type IAiToolEnablement,\n type IAiWebSearchToolConfig,\n type ModelSpec,\n type ModelSpecKey,\n allModelSpecKeys\n} from './model';\nimport { allProviderIds } from './registry';\n\n// ============================================================================\n// Provider ID\n// ============================================================================\n\n/**\n * Converter for {@link AiProviderId}.\n * @public\n */\nexport const aiProviderId: Converter<AiProviderId> = Converters.enumeratedValue<AiProviderId>(allProviderIds);\n\n// ============================================================================\n// Server-Side Tool Converters\n// ============================================================================\n\n/**\n * All known server-side tool type values.\n * @internal\n */\nconst allServerToolTypes: ReadonlyArray<AiServerToolType> = ['web_search'];\n\n/**\n * Converter for {@link AiServerToolType}.\n * @public\n */\nexport const aiServerToolType: Converter<AiServerToolType> =\n Converters.enumeratedValue<AiServerToolType>(allServerToolTypes);\n\n/**\n * Converter for {@link IAiWebSearchToolConfig}.\n * @public\n */\nexport const aiWebSearchToolConfig: Converter<IAiWebSearchToolConfig> =\n Converters.strictObject<IAiWebSearchToolConfig>({\n type: Converters.enumeratedValue<'web_search'>(['web_search']),\n allowedDomains: Converters.arrayOf(Converters.string).optional(),\n blockedDomains: Converters.arrayOf(Converters.string).optional(),\n maxUses: Converters.number.optional(),\n enableImageUnderstanding: Converters.boolean.optional()\n });\n\n/**\n * Converter for {@link AiServerToolConfig} (discriminated union on `type`).\n * @public\n */\nexport const aiServerToolConfig: Converter<AiServerToolConfig> =\n Converters.discriminatedObject<AiServerToolConfig>('type', {\n web_search: aiWebSearchToolConfig\n });\n\n// ============================================================================\n// Client-Defined Tool Converters\n// ============================================================================\n\n/**\n * Validator for the `parametersSchema` field of a client tool config.\n * Checks that the value is a non-null object exposing both `validate` and `toJson`\n * as callable functions — the runtime presence check for {@link JsonSchema.ISchemaValidator}.\n * Does not inspect the inner JSON Schema structure.\n * @internal\n */\nconst parametersSchemaValidator: Validator<JsonSchema.ISchemaValidator<unknown>> = Validators.isA<\n JsonSchema.ISchemaValidator<unknown>\n>(\n 'ISchemaValidator (must expose .validate() and .toJson())',\n (v): v is JsonSchema.ISchemaValidator<unknown> =>\n v !== null &&\n v !== undefined &&\n typeof v === 'object' &&\n typeof (v as Record<string, unknown>).validate === 'function' &&\n typeof (v as Record<string, unknown>).toJson === 'function'\n);\n\n/**\n * Converter for {@link AiAssist.IAiClientToolConfig}. Validates the wrapper shape: `type`,\n * `name`, `description`, and the presence of a usable `parametersSchema`.\n * Does not inspect the inner JSON Schema structure — `JsonSchema.object(...)` already\n * guarantees the schema is valid.\n * @public\n */\nexport const aiClientToolConfig: Converter<IAiClientToolConfig> = Converters.object<IAiClientToolConfig>({\n type: Converters.enumeratedValue<'client_tool'>(['client_tool']),\n name: Converters.string.withConstraint((s) => s.length > 0, {\n description: 'name must be a non-empty string'\n }),\n description: Converters.string,\n parametersSchema: parametersSchemaValidator\n});\n\n/**\n * Converter for {@link IAiToolEnablement}.\n * @public\n */\nexport const aiToolEnablement: Converter<IAiToolEnablement> = Converters.strictObject<IAiToolEnablement>({\n type: aiServerToolType,\n enabled: Converters.boolean,\n config: aiServerToolConfig.optional()\n});\n\n// ============================================================================\n// Model Specification\n// ============================================================================\n\n/**\n * Converter for {@link ModelSpecKey}.\n * @public\n */\nexport const modelSpecKey: Converter<ModelSpecKey> =\n Converters.enumeratedValue<ModelSpecKey>(allModelSpecKeys);\n\n/**\n * Recursive converter for {@link ModelSpec}.\n * Accepts a string or an object whose values are themselves ModelSpec values,\n * with keys constrained to known {@link ModelSpecKey} values.\n * Uses the `self` parameter from `Converters.generic` for recursion.\n * @public\n */\nexport const modelSpec: Converter<ModelSpec> = Converters.generic<ModelSpec>(\n (from: unknown, self: Converter<ModelSpec>) => {\n return Converters.oneOf<ModelSpec>([\n Converters.string,\n Converters.recordOf(self, { keyConverter: modelSpecKey })\n ])\n .withFormattedError(() => 'expected model spec (string or object with keys: base, tools, image)')\n .convert(from);\n }\n);\n\n// ============================================================================\n// Provider Config & Settings\n// ============================================================================\n\n/**\n * Converter for {@link IAiAssistProviderConfig}.\n * @public\n */\nexport const aiAssistProviderConfig: Converter<IAiAssistProviderConfig> =\n Converters.strictObject<IAiAssistProviderConfig>({\n provider: aiProviderId,\n secretName: Converters.string.optional(),\n model: modelSpec.optional(),\n tools: Converters.arrayOf(aiToolEnablement).optional(),\n endpoint: Converters.string.optional()\n });\n\n/**\n * Converter for {@link IAiAssistSettings}.\n * @public\n */\nexport const aiAssistSettings: Converter<IAiAssistSettings> = Converters.strictObject<IAiAssistSettings>({\n providers: Converters.arrayOf(aiAssistProviderConfig),\n defaultProvider: aiProviderId.optional(),\n proxyUrl: Converters.string.optional(),\n proxyAllProviders: Converters.boolean.optional()\n});\n"]}