melchizedek-agents 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/AGENT_SETUP.md +182 -0
  2. package/DOCUMENTATION.md +614 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +164 -0
  5. package/README.md +151 -0
  6. package/config/agents/README.md +27 -0
  7. package/config/agents/examples/ares.yaml +74 -0
  8. package/config/agents/examples/cartographers.yaml +115 -0
  9. package/config/agents/examples/claude.yaml +17 -0
  10. package/config/agents/examples/council.yaml +65 -0
  11. package/config/agents/examples/critic.yaml +74 -0
  12. package/config/agents/examples/delegation.yaml +29 -0
  13. package/config/agents/examples/hierarchical.yaml +26 -0
  14. package/config/agents/examples/image_production.yaml +112 -0
  15. package/config/agents/examples/librarian.yaml +75 -0
  16. package/config/agents/examples/model_zoo.yaml +81 -0
  17. package/config/agents/examples/patient_advocate.yaml +107 -0
  18. package/config/agents/examples/scriptorium.yaml +96 -0
  19. package/config/agents/examples/style_council.yaml +83 -0
  20. package/config/agents/examples/syndicate.yaml +34 -0
  21. package/config/agents/examples/tutor.yaml +66 -0
  22. package/config/agents/syndicateSchema.yaml +291 -0
  23. package/dist/lib/config.d.ts +108 -0
  24. package/dist/lib/config.js +110 -0
  25. package/dist/lib/dispatch.d.ts +133 -0
  26. package/dist/lib/dispatch.js +182 -0
  27. package/dist/lib/grounding.d.ts +28 -0
  28. package/dist/lib/grounding.js +61 -0
  29. package/dist/lib/index.d.ts +14 -0
  30. package/dist/lib/index.js +13 -0
  31. package/dist/lib/loadEnv.d.ts +21 -0
  32. package/dist/lib/loadEnv.js +58 -0
  33. package/dist/lib/loadSyndicate.d.ts +177 -0
  34. package/dist/lib/loadSyndicate.js +264 -0
  35. package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
  36. package/dist/lib/memory/supabaseMemoryService.js +499 -0
  37. package/dist/lib/models/claudeLlm.d.ts +93 -0
  38. package/dist/lib/models/claudeLlm.js +332 -0
  39. package/dist/lib/models/gptLlm.d.ts +110 -0
  40. package/dist/lib/models/gptLlm.js +497 -0
  41. package/dist/lib/models/grokLlm.d.ts +64 -0
  42. package/dist/lib/models/grokLlm.js +86 -0
  43. package/dist/lib/models/ollamaLlm.d.ts +105 -0
  44. package/dist/lib/models/ollamaLlm.js +134 -0
  45. package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
  46. package/dist/lib/models/openAiCompatibleLlm.js +331 -0
  47. package/dist/lib/models/providerMap.d.ts +28 -0
  48. package/dist/lib/models/providerMap.js +45 -0
  49. package/dist/lib/models/registry.d.ts +71 -0
  50. package/dist/lib/models/registry.js +159 -0
  51. package/dist/lib/models/schemaNormalize.d.ts +22 -0
  52. package/dist/lib/models/schemaNormalize.js +58 -0
  53. package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
  54. package/dist/lib/observability/supabaseSpanExporter.js +115 -0
  55. package/dist/lib/observability/tracer.d.ts +44 -0
  56. package/dist/lib/observability/tracer.js +242 -0
  57. package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
  58. package/dist/lib/persistence/supabaseProvider.js +88 -0
  59. package/dist/lib/session/supabaseSessionService.d.ts +26 -0
  60. package/dist/lib/session/supabaseSessionService.js +193 -0
  61. package/dist/lib/session/transcript.d.ts +193 -0
  62. package/dist/lib/session/transcript.js +309 -0
  63. package/dist/lib/toolRegistry.d.ts +15 -0
  64. package/dist/lib/toolRegistry.js +58 -0
  65. package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
  66. package/dist/lib/tools/collectionsSearchTool.js +86 -0
  67. package/dist/lib/tools/generateImageTool.d.ts +16 -0
  68. package/dist/lib/tools/generateImageTool.js +119 -0
  69. package/dist/lib/tools/inspectImageTool.d.ts +21 -0
  70. package/dist/lib/tools/inspectImageTool.js +97 -0
  71. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  72. package/dist/lib/tools/mcpToolFactory.js +116 -0
  73. package/dist/lib/tools/toolContract.d.ts +78 -0
  74. package/dist/lib/tools/toolContract.js +129 -0
  75. package/dist/lib/tools/webExtractTool.d.ts +70 -0
  76. package/dist/lib/tools/webExtractTool.js +461 -0
  77. package/dist/lib/tools/webSearchTool.d.ts +53 -0
  78. package/dist/lib/tools/webSearchTool.js +126 -0
  79. package/dist/lib/tools/wikiTools.d.ts +112 -0
  80. package/dist/lib/tools/wikiTools.js +697 -0
  81. package/dist/lib/tools/xSearchTool.d.ts +61 -0
  82. package/dist/lib/tools/xSearchTool.js +125 -0
  83. package/dist/lib/wiki/agentRun.d.ts +39 -0
  84. package/dist/lib/wiki/agentRun.js +79 -0
  85. package/dist/lib/wiki/builder.d.ts +71 -0
  86. package/dist/lib/wiki/builder.js +165 -0
  87. package/dist/lib/wiki/dive.d.ts +56 -0
  88. package/dist/lib/wiki/dive.js +208 -0
  89. package/dist/lib/wiki/entities.d.ts +220 -0
  90. package/dist/lib/wiki/entities.js +661 -0
  91. package/dist/lib/wiki/extract.d.ts +97 -0
  92. package/dist/lib/wiki/extract.js +305 -0
  93. package/dist/lib/wiki/fill.d.ts +37 -0
  94. package/dist/lib/wiki/fill.js +175 -0
  95. package/dist/lib/wiki/format.d.ts +132 -0
  96. package/dist/lib/wiki/format.js +177 -0
  97. package/dist/lib/wiki/graph.d.ts +80 -0
  98. package/dist/lib/wiki/graph.js +147 -0
  99. package/dist/lib/wiki/lint.d.ts +58 -0
  100. package/dist/lib/wiki/lint.js +267 -0
  101. package/dist/lib/wiki/markdown.d.ts +127 -0
  102. package/dist/lib/wiki/markdown.js +336 -0
  103. package/dist/lib/wiki/pipeline.d.ts +69 -0
  104. package/dist/lib/wiki/pipeline.js +119 -0
  105. package/dist/lib/wiki/vault.d.ts +84 -0
  106. package/dist/lib/wiki/vault.js +194 -0
  107. package/dist/scripts/a2a_server.d.ts +2 -0
  108. package/dist/scripts/a2a_server.js +997 -0
  109. package/dist/scripts/syndicate_chat.d.ts +2 -0
  110. package/dist/scripts/syndicate_chat.js +485 -0
  111. package/package.json +96 -0
@@ -0,0 +1,497 @@
1
+ /**
2
+ * lib/models/gptLlm.ts — OpenAI GPT provider for the ADK LLMRegistry.
3
+ *
4
+ * WHY this file exists:
5
+ * Model optionality: any agent YAML with model: "gpt-*" (or an o-series
6
+ * id like "o4-mini") routes here after registration. Unlike Grok/Ollama
7
+ * this adapter does NOT reuse the chat-completions base class — it speaks
8
+ * OpenAI's Responses API through the official `openai` SDK, because the
9
+ * Responses API is where OpenAI exposes reasoning summaries (the model's
10
+ * thinking, surfaced as { thought: true } parts here) and the first-class
11
+ * `web_search` tool this framework's web_search abstraction maps to.
12
+ *
13
+ * HOW TO ENABLE:
14
+ * 1. Install the SDK (already in package.json): npm install
15
+ * 2. Add your API key to .env: OPENAI_API_KEY=sk-...
16
+ * 3. Set model: "gpt-5-mini" (or any gpt-* / o-series id) in your YAML.
17
+ * registerAvailableProviders() registers this adapter when the key is set.
18
+ *
19
+ * CONTENT FORMAT TRANSLATION:
20
+ * ADK Content/Part objects → Responses API `input` items:
21
+ * user/model text → { role, content } message items
22
+ * functionCall part → { type:'function_call', call_id, name, arguments }
23
+ * functionResponse part → { type:'function_call_output', call_id, output }
24
+ * The call_id round-trip matters: this adapter emits functionCall.id so
25
+ * ADK echoes it back on the tool response, and the Responses API requires
26
+ * function_call_output.call_id to match the originating call.
27
+ *
28
+ * Response `output` items map back:
29
+ * 'reasoning' summary → { text, thought: true } partial (display-only)
30
+ * 'message' output_text → text part
31
+ * 'function_call' → functionCall part
32
+ * usage.input_tokens / output_tokens / output_tokens_details.reasoning_tokens
33
+ * → LlmResponse.usageMetadata, so token telemetry works like every provider.
34
+ *
35
+ * STREAMING (ADK stream=true, i.e. RunConfig streamingMode: SSE):
36
+ * The request is sent with { stream: true }; SSE deltas
37
+ * (response.output_text.delta / response.reasoning_summary_text.delta)
38
+ * are yielded as { partial: true } responses — the ADK Runner displays
39
+ * but does NOT persist partial events — and the terminal
40
+ * response.completed payload is mapped through the same final-response
41
+ * translator as the non-streaming path, carrying full content + usage
42
+ * as the ONE event that lands in the session. Function calls are never
43
+ * delta-streamed (both OpenAI and xAI deliver them whole).
44
+ */
45
+ import { BaseLlm, LLMRegistry } from '@google/adk';
46
+ import { traceLlmGeneration, setLlmSpanAttribute, } from "../observability/tracer.js";
47
+ import { wantsWebSearch, isWebSearchSentinel, xaiWebSearchParamsFromEnv, } from "../tools/webSearchTool.js";
48
+ import { wantsXSearch, isXSearchSentinel, xSearchParamsFromEnv, } from "../tools/xSearchTool.js";
49
+ import { wantsCollectionsSearch, isCollectionsSearchSentinel, collectionIdsFromEnv, collectionsMaxResultsFromEnv, } from "../tools/collectionsSearchTool.js";
50
+ import { providerForModel } from "./providerMap.js";
51
+ import { toLowercaseJsonSchema } from "./schemaNormalize.js";
52
+ /** Reasoning-capable ids: o-series and the gpt-5 family. The reasoning
53
+ * param is also dropped and retried once on a 400, so a miss here only
54
+ * costs one extra round trip. */
55
+ function isReasoningModel(model) {
56
+ return /^o[0-9]/.test(model) || /^gpt-5/.test(model);
57
+ }
58
+ // ── Request building (exported for offline tests) ────────────────────────────
59
+ /** ADK Contents → Responses API `input` items + `instructions` string. */
60
+ export function buildResponsesInput(llmRequest) {
61
+ const systemParts = [];
62
+ const input = [];
63
+ for (const content of llmRequest.contents) {
64
+ if (content.role === 'system') {
65
+ const text = content.parts
66
+ ?.filter((p) => p.text)
67
+ .map((p) => p.text)
68
+ .join('\n');
69
+ if (text)
70
+ systemParts.push(text);
71
+ continue;
72
+ }
73
+ const role = content.role === 'model' ? 'assistant' : 'user';
74
+ const contentParts = [];
75
+ for (const part of content.parts ?? []) {
76
+ const p = part;
77
+ if (p.thought) {
78
+ // Prior-turn scratchpad is display-only; never replay it.
79
+ continue;
80
+ }
81
+ if (p.text) {
82
+ contentParts.push({
83
+ type: role === 'assistant' ? 'output_text' : 'input_text',
84
+ text: p.text,
85
+ });
86
+ }
87
+ else if (p.inlineData?.data && role === 'user') {
88
+ const mime = p.inlineData.mimeType ?? 'image/png';
89
+ contentParts.push({
90
+ type: 'input_image',
91
+ image_url: `data:${mime};base64,${p.inlineData.data}`,
92
+ });
93
+ }
94
+ else if (p.functionCall) {
95
+ input.push({
96
+ type: 'function_call',
97
+ call_id: p.functionCall.id ?? `call_${input.length}`,
98
+ name: p.functionCall.name,
99
+ arguments: JSON.stringify(p.functionCall.args ?? {}),
100
+ });
101
+ }
102
+ else if (p.functionResponse) {
103
+ input.push({
104
+ type: 'function_call_output',
105
+ call_id: p.functionResponse.id ?? '',
106
+ output: JSON.stringify(p.functionResponse.response ?? {}),
107
+ });
108
+ }
109
+ }
110
+ if (contentParts.length > 0) {
111
+ input.push({ role, content: contentParts });
112
+ }
113
+ }
114
+ const configSystem = llmRequest.config?.systemInstruction;
115
+ if (configSystem) {
116
+ const text = typeof configSystem === 'string'
117
+ ? configSystem
118
+ : configSystem.parts?.map((p) => p.text).join('\n') ?? '';
119
+ if (text)
120
+ systemParts.unshift(text);
121
+ }
122
+ return {
123
+ instructions: systemParts.length > 0 ? systemParts.join('\n\n') : undefined,
124
+ input,
125
+ };
126
+ }
127
+ /** ADK toolsDict (+ web_search sentinel) → Responses API tool definitions. */
128
+ export function buildResponsesTools(llmRequest) {
129
+ const tools = [];
130
+ for (const [, tool] of Object.entries(llmRequest.toolsDict ?? {})) {
131
+ if (isWebSearchSentinel(tool))
132
+ continue; // added as a native tool below
133
+ if (isXSearchSentinel(tool))
134
+ continue; // added as a native tool below
135
+ if (isCollectionsSearchSentinel(tool))
136
+ continue; // added as a native tool below
137
+ const t = tool;
138
+ if (t.name && t.description) {
139
+ tools.push({
140
+ type: 'function',
141
+ name: t.name,
142
+ description: t.description,
143
+ parameters: toLowercaseJsonSchema(t.parameters ?? { type: 'object', properties: {} }),
144
+ strict: false,
145
+ });
146
+ }
147
+ }
148
+ if (wantsWebSearch(llmRequest)) {
149
+ // OpenAI-native, runs server-side. On the xAI path only, optional
150
+ // domain filters ride the tool object (XAI_WEB_SEARCH_* env vars —
151
+ // deployment config, the XAI_COLLECTION_IDS doctrine); xAI's web_search
152
+ // accepts no date bounds (docs.x.ai, 2026-08-08 — from_date/to_date are
153
+ // x_search-only). OpenAI's web_search takes no params and stays bare.
154
+ const xaiParams = llmRequest.model && providerForModel(llmRequest.model) === 'xai'
155
+ ? xaiWebSearchParamsFromEnv()
156
+ : {};
157
+ tools.push({ type: 'web_search', ...xaiParams });
158
+ }
159
+ if (wantsXSearch(llmRequest)) {
160
+ // xAI Agent Tools only (sentinel is xai-gated). Optional server-side
161
+ // constraints (date bounds, handle lists) are deployment config from
162
+ // XAI_X_SEARCH_* env vars — the XAI_COLLECTION_IDS doctrine; with none
163
+ // set this is the bare tool it always was.
164
+ tools.push({ type: 'x_search', ...xSearchParamsFromEnv() });
165
+ }
166
+ if (wantsCollectionsSearch(llmRequest)) {
167
+ // xAI Collections ride the OpenAI-compatible `file_search` wire shape;
168
+ // ids are deployment config (XAI_COLLECTION_IDS), never YAML.
169
+ const ids = collectionIdsFromEnv();
170
+ if (ids.length > 0) {
171
+ const max = collectionsMaxResultsFromEnv();
172
+ tools.push({
173
+ type: 'file_search',
174
+ vector_store_ids: ids,
175
+ ...(max !== undefined ? { max_num_results: max } : {}),
176
+ });
177
+ }
178
+ else {
179
+ console.warn('[collections_search] declared but XAI_COLLECTION_IDS is empty — tool omitted for this request.');
180
+ }
181
+ }
182
+ return tools;
183
+ }
184
+ // ── Stream-event mapping (exported for offline tests) ────────────────────────
185
+ /** Maps one Responses-API SSE event to a displayable delta, or null for
186
+ * event types that carry none. Function calls are NOT delta-streamed —
187
+ * both OpenAI and xAI deliver them whole in the final response. */
188
+ export function streamEventDelta(ev) {
189
+ if (!ev || typeof ev !== 'object')
190
+ return null;
191
+ if (ev.type === 'response.output_text.delta' && typeof ev.delta === 'string') {
192
+ return { thought: false, text: ev.delta };
193
+ }
194
+ if (ev.type === 'response.reasoning_summary_text.delta' &&
195
+ typeof ev.delta === 'string') {
196
+ return { thought: true, text: ev.delta };
197
+ }
198
+ return null;
199
+ }
200
+ // ── GptLlm ───────────────────────────────────────────────────────────────────
201
+ export class GptLlm extends BaseLlm {
202
+ /** gpt-* and o-series ids route here after registration. */
203
+ static supportedModels = [
204
+ /^gpt-.+/,
205
+ /^o[0-9].*/,
206
+ ];
207
+ apiKey;
208
+ constructor({ model, apiKey }) {
209
+ super({ model });
210
+ this.apiKey = apiKey;
211
+ }
212
+ // ── Provider hooks ─────────────────────────────────────────────────────────
213
+ // The Responses API surface is spoken by more than one vendor: xAI's Agent
214
+ // Tools API (lib/models/grokLlm.ts) is wire-compatible, so GrokLlm
215
+ // subclasses this adapter and overrides only these hooks.
216
+ /** Provider id for telemetry and error codes. */
217
+ providerId() {
218
+ return 'openai';
219
+ }
220
+ /** SDK baseURL override; undefined = api.openai.com. */
221
+ baseURL() {
222
+ return undefined;
223
+ }
224
+ apiKeyFromEnv() {
225
+ return process.env.OPENAI_API_KEY;
226
+ }
227
+ missingKeyMessage() {
228
+ return 'OPENAI_API_KEY is not set in environment.';
229
+ }
230
+ /** Responses API `reasoning` request param, or undefined to omit it.
231
+ * Base: reasoning summaries for OpenAI's reasoning-capable ids.
232
+ * Subclasses override per vendor (GrokLlm pins grok-4.5 to a reasoning
233
+ * effort). A 400 from a model that rejects the param is retried once
234
+ * without it — see createWithRetry. */
235
+ reasoningParam() {
236
+ return isReasoningModel(this.model) ? { summary: 'auto' } : undefined;
237
+ }
238
+ /** Extra options for the OpenAI SDK client constructor. Subclasses
239
+ * override per vendor (GrokLlm sets a long request timeout, per xAI's
240
+ * streaming guidance for reasoning models). */
241
+ clientOptions() {
242
+ return {};
243
+ }
244
+ // ── Generation ─────────────────────────────────────────────────────────────
245
+ async *generateContentAsync(llmRequest, stream = false) {
246
+ yield* traceLlmGeneration({ provider: this.providerId(), model: this.model }, this.generateInner(llmRequest, stream));
247
+ }
248
+ async *generateInner(llmRequest, stream) {
249
+ const apiKey = this.apiKey || this.apiKeyFromEnv();
250
+ if (!apiKey) {
251
+ yield {
252
+ errorCode: 'MISSING_API_KEY',
253
+ errorMessage: this.missingKeyMessage(),
254
+ };
255
+ return;
256
+ }
257
+ // Dynamic import — mirrors claudeLlm.ts, so the framework boots without
258
+ // the openai SDK installed for users of other providers.
259
+ let OpenAI;
260
+ try {
261
+ const mod = await import('openai');
262
+ OpenAI = mod.default ?? mod.OpenAI;
263
+ }
264
+ catch {
265
+ yield {
266
+ errorCode: 'SDK_NOT_INSTALLED',
267
+ errorMessage: 'The openai package is not installed. Run: npm install openai',
268
+ };
269
+ return;
270
+ }
271
+ const client = new OpenAI({
272
+ apiKey,
273
+ ...(this.baseURL() ? { baseURL: this.baseURL() } : {}),
274
+ ...this.clientOptions(),
275
+ });
276
+ const { instructions, input } = buildResponsesInput(llmRequest);
277
+ const tools = buildResponsesTools(llmRequest);
278
+ if (wantsWebSearch(llmRequest)) {
279
+ setLlmSpanAttribute('llm.web_search.native', true);
280
+ }
281
+ if (wantsCollectionsSearch(llmRequest)) {
282
+ setLlmSpanAttribute(collectionIdsFromEnv().length > 0
283
+ ? 'llm.collections_search.native'
284
+ : 'llm.collections_search.omitted', true);
285
+ }
286
+ const cfg = llmRequest.config ?? {};
287
+ const reasoning = this.reasoningParam();
288
+ const request = {
289
+ model: this.model,
290
+ input,
291
+ ...(instructions ? { instructions } : {}),
292
+ ...(tools.length > 0 ? { tools } : {}),
293
+ ...(cfg.maxOutputTokens !== undefined
294
+ ? { max_output_tokens: cfg.maxOutputTokens }
295
+ : {}),
296
+ ...(cfg.temperature !== undefined && !isReasoningModel(this.model)
297
+ ? { temperature: cfg.temperature }
298
+ : {}),
299
+ // Structured output: outputSchema wins over bare JSON mode.
300
+ ...(cfg.responseSchema
301
+ ? {
302
+ text: {
303
+ format: {
304
+ type: 'json_schema',
305
+ name: 'response',
306
+ schema: toLowercaseJsonSchema(cfg.responseSchema),
307
+ },
308
+ },
309
+ }
310
+ : cfg.responseMimeType === 'application/json'
311
+ ? { text: { format: { type: 'json_object' } } }
312
+ : {}),
313
+ // Reasoning param — summaries and/or vendor effort control (see
314
+ // reasoningParam hook; provider subclasses shape it).
315
+ ...(reasoning ? { reasoning } : {}),
316
+ };
317
+ try {
318
+ if (stream) {
319
+ yield* this.streamResponses(client, request);
320
+ return;
321
+ }
322
+ const response = await this.createWithRetry(client, request);
323
+ yield* this.mapFinalResponse(response);
324
+ }
325
+ catch (err) {
326
+ const msg = err instanceof Error ? err.message : String(err);
327
+ yield {
328
+ errorCode: `${this.providerId().toUpperCase()}_ERROR`,
329
+ errorMessage: msg,
330
+ };
331
+ }
332
+ }
333
+ /** responses.create with the guarded reasoning retry: if the model
334
+ * rejects the reasoning param with a 400 (a non-reasoning model matched
335
+ * the pattern), drop it and try once more. */
336
+ async createWithRetry(client, request) {
337
+ try {
338
+ return await client.responses.create(request);
339
+ }
340
+ catch (err) {
341
+ if (err?.status === 400 && request.reasoning) {
342
+ delete request.reasoning;
343
+ return await client.responses.create(request);
344
+ }
345
+ throw err;
346
+ }
347
+ }
348
+ /** Final (non-delta) Response → LlmResponse yields: reasoning summaries
349
+ * as a display-only thought part, then text/functionCall parts with
350
+ * usage. Shared by the non-streaming path and the stream finalizer
351
+ * (which sets skipThoughts — its summaries already streamed as deltas). */
352
+ *mapFinalResponse(response, opts = {}) {
353
+ if (!opts.skipThoughts) {
354
+ const reasoningTexts = [];
355
+ for (const item of response.output ?? []) {
356
+ if (item.type === 'reasoning') {
357
+ for (const s of item.summary ?? []) {
358
+ if (s?.text)
359
+ reasoningTexts.push(s.text);
360
+ }
361
+ }
362
+ }
363
+ if (reasoningTexts.length > 0) {
364
+ yield {
365
+ content: {
366
+ role: 'model',
367
+ parts: [{ text: reasoningTexts.join('\n\n'), thought: true }],
368
+ },
369
+ partial: true,
370
+ };
371
+ }
372
+ }
373
+ const parts = [];
374
+ for (const item of response.output ?? []) {
375
+ if (item.type === 'message') {
376
+ for (const c of item.content ?? []) {
377
+ if (c.type === 'output_text' && c.text)
378
+ parts.push({ text: c.text });
379
+ }
380
+ }
381
+ else if (item.type === 'function_call') {
382
+ let args = {};
383
+ try {
384
+ args = JSON.parse(item.arguments ?? '{}');
385
+ }
386
+ catch {
387
+ args = { raw: item.arguments };
388
+ }
389
+ parts.push({
390
+ functionCall: { name: item.name, args, id: item.call_id },
391
+ });
392
+ }
393
+ }
394
+ const usage = response.usage;
395
+ yield {
396
+ content: { role: 'model', parts },
397
+ turnComplete: true,
398
+ ...(usage
399
+ ? {
400
+ usageMetadata: {
401
+ ...(usage.input_tokens !== undefined
402
+ ? { promptTokenCount: usage.input_tokens }
403
+ : {}),
404
+ ...(usage.output_tokens !== undefined
405
+ ? { candidatesTokenCount: usage.output_tokens }
406
+ : {}),
407
+ ...(usage.output_tokens_details?.reasoning_tokens !== undefined
408
+ ? {
409
+ thoughtsTokenCount: usage.output_tokens_details.reasoning_tokens,
410
+ }
411
+ : {}),
412
+ ...(usage.total_tokens !== undefined
413
+ ? { totalTokenCount: usage.total_tokens }
414
+ : {}),
415
+ },
416
+ }
417
+ : {}),
418
+ };
419
+ }
420
+ /** SSE streaming: yield displayable DELTAS as { partial: true } responses
421
+ * (the ADK Runner shows but never persists partials), then map the
422
+ * terminal response.completed payload through mapFinalResponse — full
423
+ * content + usage, the ONE event that lands in the session. Usage rides
424
+ * only the final response so the tracer never double-counts tokens. */
425
+ async *streamResponses(client, request) {
426
+ const events = await this.createWithRetry(client, {
427
+ ...request,
428
+ stream: true,
429
+ });
430
+ let finalResponse;
431
+ let streamedThoughts = false;
432
+ const textBuf = [];
433
+ const thoughtBuf = [];
434
+ for await (const ev of events) {
435
+ const delta = streamEventDelta(ev);
436
+ if (delta) {
437
+ if (delta.thought) {
438
+ streamedThoughts = true;
439
+ thoughtBuf.push(delta.text);
440
+ }
441
+ else {
442
+ textBuf.push(delta.text);
443
+ }
444
+ yield {
445
+ content: {
446
+ role: 'model',
447
+ parts: [
448
+ delta.thought
449
+ ? { text: delta.text, thought: true }
450
+ : { text: delta.text },
451
+ ],
452
+ },
453
+ partial: true,
454
+ };
455
+ continue;
456
+ }
457
+ if (ev?.type === 'response.completed' && ev.response) {
458
+ finalResponse = ev.response;
459
+ }
460
+ else if (ev?.type === 'response.failed' || ev?.type === 'error') {
461
+ const msg = ev?.response?.error?.message ?? ev?.message ?? 'response stream failed';
462
+ yield {
463
+ errorCode: `${this.providerId().toUpperCase()}_STREAM_ERROR`,
464
+ errorMessage: String(msg),
465
+ };
466
+ return;
467
+ }
468
+ }
469
+ if (finalResponse) {
470
+ yield* this.mapFinalResponse(finalResponse, {
471
+ skipThoughts: streamedThoughts,
472
+ });
473
+ return;
474
+ }
475
+ // Defensive: the stream ended without response.completed — aggregate
476
+ // the buffered deltas so the turn still persists a complete event.
477
+ const parts = [];
478
+ if (thoughtBuf.length > 0)
479
+ parts.push({ text: thoughtBuf.join(''), thought: true });
480
+ if (textBuf.length > 0)
481
+ parts.push({ text: textBuf.join('') });
482
+ yield { content: { role: 'model', parts }, turnComplete: true };
483
+ }
484
+ /** Live/bidirectional streaming is not wired for this adapter. */
485
+ async connect(_llmRequest) {
486
+ throw new Error('GptLlm does not support live bidirectional connections. ' +
487
+ 'Use a Gemini model for live/streaming sessions.');
488
+ }
489
+ }
490
+ // ── Registration helper ───────────────────────────────────────────────────────
491
+ /**
492
+ * Registers GptLlm with the ADK LLMRegistry. Called by
493
+ * registerAvailableProviders() when OPENAI_API_KEY is present.
494
+ */
495
+ export function registerGptLlm() {
496
+ LLMRegistry.register(GptLlm);
497
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * lib/models/grokLlm.ts — xAI Grok provider for the ADK LLMRegistry.
3
+ *
4
+ * WHY this file exists:
5
+ * Model optionality is a primary driver of this framework: the agent YAML
6
+ * declares `model`, and the registry routes it to the right provider.
7
+ *
8
+ * WHY it subclasses GptLlm (Responses API) and not the chat-completions base:
9
+ * xAI retired Live Search on chat completions (the API now returns
10
+ * 410 "Live search is deprecated. Please switch to the Agent Tools API").
11
+ * The Agent Tools API lives at `https://api.x.ai/v1/responses` and is
12
+ * wire-compatible with OpenAI's Responses API — same `input` items, same
13
+ * `{ type: 'function' }` / `{ type: 'web_search' }` tools, same
14
+ * `reasoning` summary / `message` / `function_call` output items (verified
15
+ * live 2026-07-19). So Grok reuses the GptLlm translator via the openai
16
+ * SDK's baseURL override, and gets for free:
17
+ * - native web_search (server-side, with source citations)
18
+ * - x_search (live X posts) and collections_search (hosted document
19
+ * stores via the file_search wire shape — see collectionsSearchTool.ts)
20
+ * - reasoning summaries surfaced as { thought: true } THINKING output
21
+ * - SSE streaming (ADK RunConfig streamingMode: SSE → partial deltas)
22
+ * - structured outputs (YAML outputSchema → text.format json_schema)
23
+ * - function tools with call_id round-tripping, lowercased schemas
24
+ * - usage → usageMetadata mapping for llm.request token spans
25
+ *
26
+ * HOW TO ENABLE:
27
+ * 1. Add your API key to .env: XAI_API_KEY=xai-... (console.x.ai — paid)
28
+ * 2. Set model: "grok-4.5" (or any grok-* id) in your YAML.
29
+ * registerAvailableProviders() registers this adapter when the key is set.
30
+ * grok-4.5 requests carry reasoning effort 'medium' by default — see
31
+ * reasoningParam() below and DEFAULT_GROK_REASONING_EFFORT (lib/config.ts).
32
+ *
33
+ * API-DRIFT NOTE: every xAI-specific choice (endpoint, key env) is confined
34
+ * to the overrides below — upstream drift stays a one-file fix. Note xAI's
35
+ * Responses endpoint reports the serving backend in the response `model`
36
+ * field (e.g. "grok-4.3"), which may differ from the requested id; invalid
37
+ * ids are properly rejected with "Model not found".
38
+ */
39
+ import { GptLlm } from './gptLlm.ts';
40
+ export declare class GrokLlm extends GptLlm {
41
+ /** Any model: "grok-*" in a YAML config routes here after registration. */
42
+ static readonly supportedModels: Array<string | RegExp>;
43
+ protected providerId(): string;
44
+ protected baseURL(): string;
45
+ protected apiKeyFromEnv(): string | undefined;
46
+ protected missingKeyMessage(): string;
47
+ /** xAI's streaming docs advise a long request timeout for reasoning
48
+ * models (their examples use 3600s) so slow thinking can't trip the
49
+ * SDK's default mid-stream. Applied to all Grok requests. */
50
+ protected clientOptions(): Record<string, unknown>;
51
+ /** grok-4.5 exposes reasoning-effort control ('low' | 'medium' | 'high';
52
+ * xAI defaults to 'high', and reasoning cannot be disabled) via the
53
+ * Responses API `reasoning.effort` field — docs.x.ai › Model
54
+ * capabilities › Text › Reasoning › Effort levels. We pin
55
+ * DEFAULT_GROK_REASONING_EFFORT (medium). Other grok ids don't accept
56
+ * the param and get none (their reasoning summaries arrive unrequested);
57
+ * if xAI ever rejects it, GptLlm's guarded 400 retry drops it. */
58
+ protected reasoningParam(): Record<string, unknown> | undefined;
59
+ }
60
+ /**
61
+ * Registers GrokLlm with the ADK LLMRegistry. Called by
62
+ * registerAvailableProviders() when XAI_API_KEY is present.
63
+ */
64
+ export declare function registerGrokLlm(): void;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * lib/models/grokLlm.ts — xAI Grok provider for the ADK LLMRegistry.
3
+ *
4
+ * WHY this file exists:
5
+ * Model optionality is a primary driver of this framework: the agent YAML
6
+ * declares `model`, and the registry routes it to the right provider.
7
+ *
8
+ * WHY it subclasses GptLlm (Responses API) and not the chat-completions base:
9
+ * xAI retired Live Search on chat completions (the API now returns
10
+ * 410 "Live search is deprecated. Please switch to the Agent Tools API").
11
+ * The Agent Tools API lives at `https://api.x.ai/v1/responses` and is
12
+ * wire-compatible with OpenAI's Responses API — same `input` items, same
13
+ * `{ type: 'function' }` / `{ type: 'web_search' }` tools, same
14
+ * `reasoning` summary / `message` / `function_call` output items (verified
15
+ * live 2026-07-19). So Grok reuses the GptLlm translator via the openai
16
+ * SDK's baseURL override, and gets for free:
17
+ * - native web_search (server-side, with source citations)
18
+ * - x_search (live X posts) and collections_search (hosted document
19
+ * stores via the file_search wire shape — see collectionsSearchTool.ts)
20
+ * - reasoning summaries surfaced as { thought: true } THINKING output
21
+ * - SSE streaming (ADK RunConfig streamingMode: SSE → partial deltas)
22
+ * - structured outputs (YAML outputSchema → text.format json_schema)
23
+ * - function tools with call_id round-tripping, lowercased schemas
24
+ * - usage → usageMetadata mapping for llm.request token spans
25
+ *
26
+ * HOW TO ENABLE:
27
+ * 1. Add your API key to .env: XAI_API_KEY=xai-... (console.x.ai — paid)
28
+ * 2. Set model: "grok-4.5" (or any grok-* id) in your YAML.
29
+ * registerAvailableProviders() registers this adapter when the key is set.
30
+ * grok-4.5 requests carry reasoning effort 'medium' by default — see
31
+ * reasoningParam() below and DEFAULT_GROK_REASONING_EFFORT (lib/config.ts).
32
+ *
33
+ * API-DRIFT NOTE: every xAI-specific choice (endpoint, key env) is confined
34
+ * to the overrides below — upstream drift stays a one-file fix. Note xAI's
35
+ * Responses endpoint reports the serving backend in the response `model`
36
+ * field (e.g. "grok-4.3"), which may differ from the requested id; invalid
37
+ * ids are properly rejected with "Model not found".
38
+ */
39
+ import { LLMRegistry } from '@google/adk';
40
+ import { DEFAULT_GROK_REASONING_EFFORT } from "../config.js";
41
+ import { GptLlm } from "./gptLlm.js";
42
+ const XAI_BASE_URL = 'https://api.x.ai/v1';
43
+ // ── GrokLlm ───────────────────────────────────────────────────────────────────
44
+ export class GrokLlm extends GptLlm {
45
+ /** Any model: "grok-*" in a YAML config routes here after registration. */
46
+ static supportedModels = [/^grok-.+/];
47
+ providerId() {
48
+ return 'xai';
49
+ }
50
+ baseURL() {
51
+ return XAI_BASE_URL;
52
+ }
53
+ apiKeyFromEnv() {
54
+ return process.env.XAI_API_KEY;
55
+ }
56
+ missingKeyMessage() {
57
+ return 'XAI_API_KEY is not set in environment.';
58
+ }
59
+ /** xAI's streaming docs advise a long request timeout for reasoning
60
+ * models (their examples use 3600s) so slow thinking can't trip the
61
+ * SDK's default mid-stream. Applied to all Grok requests. */
62
+ clientOptions() {
63
+ return { timeout: 3_600_000 };
64
+ }
65
+ /** grok-4.5 exposes reasoning-effort control ('low' | 'medium' | 'high';
66
+ * xAI defaults to 'high', and reasoning cannot be disabled) via the
67
+ * Responses API `reasoning.effort` field — docs.x.ai › Model
68
+ * capabilities › Text › Reasoning › Effort levels. We pin
69
+ * DEFAULT_GROK_REASONING_EFFORT (medium). Other grok ids don't accept
70
+ * the param and get none (their reasoning summaries arrive unrequested);
71
+ * if xAI ever rejects it, GptLlm's guarded 400 retry drops it. */
72
+ reasoningParam() {
73
+ if (/^grok-4\.5/.test(this.model)) {
74
+ return { effort: DEFAULT_GROK_REASONING_EFFORT };
75
+ }
76
+ return undefined;
77
+ }
78
+ }
79
+ // ── Registration helper ───────────────────────────────────────────────────────
80
+ /**
81
+ * Registers GrokLlm with the ADK LLMRegistry. Called by
82
+ * registerAvailableProviders() when XAI_API_KEY is present.
83
+ */
84
+ export function registerGrokLlm() {
85
+ LLMRegistry.register(GrokLlm);
86
+ }