ei-tui 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/core/llm-client.ts
CHANGED
|
@@ -336,10 +336,21 @@ export async function callLLMRaw(
|
|
|
336
336
|
console.log(`[LLM] Extended thinking detected (${thinking.length} chars)`);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
let finalToolCalls = rawToolCalls;
|
|
340
|
+
if ((!rawToolCalls || rawToolCalls.length === 0) && choice?.finish_reason === "stop" && typeof textContent === "string") {
|
|
341
|
+
const rescued = rescueGemmaToolCalls(textContent);
|
|
342
|
+
if (rescued.length > 0) {
|
|
343
|
+
console.log(`[LLM] Rescued ${rescued.length} tool call(s) from content (Gemma native format)`);
|
|
344
|
+
finalToolCalls = rescued;
|
|
345
|
+
textContent = null;
|
|
346
|
+
if (choice) (choice as Record<string, unknown>).finish_reason = "tool_calls";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
339
350
|
return {
|
|
340
351
|
content: textContent,
|
|
341
352
|
finishReason: choice?.finish_reason ?? null,
|
|
342
|
-
rawToolCalls,
|
|
353
|
+
rawToolCalls: finalToolCalls,
|
|
343
354
|
assistantMessage,
|
|
344
355
|
...(thinking ? { thinking } : {}),
|
|
345
356
|
};
|
|
@@ -395,6 +406,57 @@ export function repairJSON(jsonStr: string): string {
|
|
|
395
406
|
return repaired;
|
|
396
407
|
}
|
|
397
408
|
|
|
409
|
+
// =============================================================================
|
|
410
|
+
// Gemma native tool call rescue
|
|
411
|
+
// =============================================================================
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Gemma (via LM Studio) occasionally emits tool calls in `content` instead of
|
|
415
|
+
* `tool_calls`, using its native token format:
|
|
416
|
+
*
|
|
417
|
+
* <|tool_call>call:FUNCTION{param:<|"|>string value<|"|>,bool:true}<tool_call|>
|
|
418
|
+
*
|
|
419
|
+
* This parser extracts those calls and converts them to OpenAI-compatible shape
|
|
420
|
+
* so the rest of the pipeline (parseToolCalls → executeToolCalls) sees a clean
|
|
421
|
+
* contract. Call it when finish_reason is "stop" and tool_calls is empty.
|
|
422
|
+
*/
|
|
423
|
+
export function rescueGemmaToolCalls(content: string): unknown[] {
|
|
424
|
+
const CALL_RE = /<\|tool_call>call:(\w+)\{([\s\S]*?)\}<tool_call\|>/g;
|
|
425
|
+
const STRING_PARAM_RE = /(\w+):<\|"?\|>([\s\S]*?)<\|"?\|>/g;
|
|
426
|
+
const SCALAR_PARAM_RE = /(\w+):(true|false|-?\d+\.?\d*)/g;
|
|
427
|
+
|
|
428
|
+
const rescued: unknown[] = [];
|
|
429
|
+
let callMatch: RegExpExecArray | null;
|
|
430
|
+
|
|
431
|
+
while ((callMatch = CALL_RE.exec(content)) !== null) {
|
|
432
|
+
const fnName = callMatch[1];
|
|
433
|
+
const argsStr = callMatch[2];
|
|
434
|
+
const args: Record<string, unknown> = {};
|
|
435
|
+
|
|
436
|
+
let m: RegExpExecArray | null;
|
|
437
|
+
STRING_PARAM_RE.lastIndex = 0;
|
|
438
|
+
while ((m = STRING_PARAM_RE.exec(argsStr)) !== null) {
|
|
439
|
+
args[m[1]] = m[2];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
SCALAR_PARAM_RE.lastIndex = 0;
|
|
443
|
+
while ((m = SCALAR_PARAM_RE.exec(argsStr)) !== null) {
|
|
444
|
+
if (!(m[1] in args)) {
|
|
445
|
+
const v = m[2];
|
|
446
|
+
args[m[1]] = v === "true" ? true : v === "false" ? false : Number(v);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
rescued.push({
|
|
451
|
+
id: crypto.randomUUID(),
|
|
452
|
+
type: "function",
|
|
453
|
+
function: { name: fnName, arguments: JSON.stringify(args) },
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return rescued;
|
|
458
|
+
}
|
|
459
|
+
|
|
398
460
|
export function parseJSONResponse(content: string): unknown {
|
|
399
461
|
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
400
462
|
const jsonStr = jsonMatch ? jsonMatch[1].trim() : content.trim();
|
package/src/core/processor.ts
CHANGED
|
@@ -301,18 +301,17 @@ export class Processor {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// read_memory tool
|
|
304
|
-
|
|
305
|
-
this.stateManager.tools_add({
|
|
304
|
+
this.stateManager.tools_upsertBuiltin({
|
|
306
305
|
id: crypto.randomUUID(),
|
|
307
306
|
provider_id: "ei",
|
|
308
307
|
name: "read_memory",
|
|
309
308
|
display_name: "Read Memory",
|
|
310
309
|
description:
|
|
311
|
-
"Search
|
|
310
|
+
"Search Ei's persistent knowledge base — facts, topics, people, and quotes learned across ALL conversations over time, not just this one. Use this when you need context about the user, their life, relationships, or interests that may not be visible in the current exchange. Use `recent: true` to retrieve what's been discussed recently.",
|
|
312
311
|
input_schema: {
|
|
313
312
|
type: "object",
|
|
314
313
|
properties: {
|
|
315
|
-
query: { type: "string", description: "What to search for
|
|
314
|
+
query: { type: "string", description: "What to search for — a person, topic, fact, or anything Ei has learned about the user" },
|
|
316
315
|
types: {
|
|
317
316
|
type: "array",
|
|
318
317
|
items: { type: "string", enum: ["fact", "topic", "person", "quote"] },
|
|
@@ -328,12 +327,10 @@ export class Processor {
|
|
|
328
327
|
enabled: true,
|
|
329
328
|
created_at: now,
|
|
330
329
|
max_calls_per_interaction: 6, // Dedup needs to verify relationships before irreversible merges. Typical cluster (3-8 items) requires: parent concept lookup + 2 relationship verifications + context validation. Still under HARD_TOOL_CALL_LIMIT (10).
|
|
331
|
-
|
|
332
|
-
}
|
|
330
|
+
});
|
|
333
331
|
|
|
334
332
|
// file_read tool (TUI only)
|
|
335
|
-
|
|
336
|
-
this.stateManager.tools_add({
|
|
333
|
+
this.stateManager.tools_upsertBuiltin({
|
|
337
334
|
id: crypto.randomUUID(),
|
|
338
335
|
provider_id: "ei",
|
|
339
336
|
name: "file_read",
|
|
@@ -352,12 +349,10 @@ export class Processor {
|
|
|
352
349
|
enabled: true,
|
|
353
350
|
created_at: now,
|
|
354
351
|
max_calls_per_interaction: 5,
|
|
355
|
-
|
|
356
|
-
}
|
|
352
|
+
});
|
|
357
353
|
|
|
358
354
|
// list_directory tool (TUI only)
|
|
359
|
-
|
|
360
|
-
this.stateManager.tools_add({
|
|
355
|
+
this.stateManager.tools_upsertBuiltin({
|
|
361
356
|
id: crypto.randomUUID(),
|
|
362
357
|
provider_id: "ei",
|
|
363
358
|
name: "list_directory",
|
|
@@ -376,12 +371,10 @@ export class Processor {
|
|
|
376
371
|
enabled: true,
|
|
377
372
|
created_at: now,
|
|
378
373
|
max_calls_per_interaction: 5,
|
|
379
|
-
|
|
380
|
-
}
|
|
374
|
+
});
|
|
381
375
|
|
|
382
376
|
// directory_tree tool (TUI only)
|
|
383
|
-
|
|
384
|
-
this.stateManager.tools_add({
|
|
377
|
+
this.stateManager.tools_upsertBuiltin({
|
|
385
378
|
id: crypto.randomUUID(),
|
|
386
379
|
provider_id: "ei",
|
|
387
380
|
name: "directory_tree",
|
|
@@ -401,12 +394,10 @@ export class Processor {
|
|
|
401
394
|
enabled: true,
|
|
402
395
|
created_at: now,
|
|
403
396
|
max_calls_per_interaction: 3,
|
|
404
|
-
|
|
405
|
-
}
|
|
397
|
+
});
|
|
406
398
|
|
|
407
399
|
// search_files tool (TUI only)
|
|
408
|
-
|
|
409
|
-
this.stateManager.tools_add({
|
|
400
|
+
this.stateManager.tools_upsertBuiltin({
|
|
410
401
|
id: crypto.randomUUID(),
|
|
411
402
|
provider_id: "ei",
|
|
412
403
|
name: "search_files",
|
|
@@ -426,12 +417,10 @@ export class Processor {
|
|
|
426
417
|
enabled: true,
|
|
427
418
|
created_at: now,
|
|
428
419
|
max_calls_per_interaction: 3,
|
|
429
|
-
|
|
430
|
-
}
|
|
420
|
+
});
|
|
431
421
|
|
|
432
422
|
// grep tool (TUI only)
|
|
433
|
-
|
|
434
|
-
this.stateManager.tools_add({
|
|
423
|
+
this.stateManager.tools_upsertBuiltin({
|
|
435
424
|
id: crypto.randomUUID(),
|
|
436
425
|
provider_id: "ei",
|
|
437
426
|
name: "grep",
|
|
@@ -453,12 +442,10 @@ export class Processor {
|
|
|
453
442
|
enabled: true,
|
|
454
443
|
created_at: now,
|
|
455
444
|
max_calls_per_interaction: 5,
|
|
456
|
-
|
|
457
|
-
}
|
|
445
|
+
});
|
|
458
446
|
|
|
459
447
|
// get_file_info tool (TUI only)
|
|
460
|
-
|
|
461
|
-
this.stateManager.tools_add({
|
|
448
|
+
this.stateManager.tools_upsertBuiltin({
|
|
462
449
|
id: crypto.randomUUID(),
|
|
463
450
|
provider_id: "ei",
|
|
464
451
|
name: "get_file_info",
|
|
@@ -477,12 +464,10 @@ export class Processor {
|
|
|
477
464
|
enabled: true,
|
|
478
465
|
created_at: now,
|
|
479
466
|
max_calls_per_interaction: 5,
|
|
480
|
-
|
|
481
|
-
}
|
|
467
|
+
});
|
|
482
468
|
|
|
483
469
|
// web_fetch tool
|
|
484
|
-
|
|
485
|
-
this.stateManager.tools_add({
|
|
470
|
+
this.stateManager.tools_upsertBuiltin({
|
|
486
471
|
id: crypto.randomUUID(),
|
|
487
472
|
provider_id: "ei",
|
|
488
473
|
name: "web_fetch",
|
|
@@ -501,8 +486,7 @@ export class Processor {
|
|
|
501
486
|
enabled: true,
|
|
502
487
|
created_at: now,
|
|
503
488
|
max_calls_per_interaction: 3,
|
|
504
|
-
|
|
505
|
-
}
|
|
489
|
+
});
|
|
506
490
|
|
|
507
491
|
// --- Tavily Search provider ---
|
|
508
492
|
if (!this.stateManager.tools_getProviderById("tavily")) {
|
|
@@ -521,8 +505,7 @@ export class Processor {
|
|
|
521
505
|
}
|
|
522
506
|
|
|
523
507
|
// tavily_web_search
|
|
524
|
-
|
|
525
|
-
this.stateManager.tools_add({
|
|
508
|
+
this.stateManager.tools_upsertBuiltin({
|
|
526
509
|
id: crypto.randomUUID(),
|
|
527
510
|
provider_id: "tavily",
|
|
528
511
|
name: "tavily_web_search",
|
|
@@ -542,12 +525,10 @@ export class Processor {
|
|
|
542
525
|
enabled: true,
|
|
543
526
|
created_at: now,
|
|
544
527
|
max_calls_per_interaction: 3,
|
|
545
|
-
|
|
546
|
-
}
|
|
528
|
+
});
|
|
547
529
|
|
|
548
530
|
// tavily_news_search
|
|
549
|
-
|
|
550
|
-
this.stateManager.tools_add({
|
|
531
|
+
this.stateManager.tools_upsertBuiltin({
|
|
551
532
|
id: crypto.randomUUID(),
|
|
552
533
|
provider_id: "tavily",
|
|
553
534
|
name: "tavily_news_search",
|
|
@@ -567,8 +548,7 @@ export class Processor {
|
|
|
567
548
|
enabled: true,
|
|
568
549
|
created_at: now,
|
|
569
550
|
max_calls_per_interaction: 3,
|
|
570
|
-
|
|
571
|
-
}
|
|
551
|
+
});
|
|
572
552
|
|
|
573
553
|
// --- Spotify provider ---
|
|
574
554
|
if (!this.stateManager.tools_getProviderById("spotify")) {
|
|
@@ -587,8 +567,7 @@ export class Processor {
|
|
|
587
567
|
}
|
|
588
568
|
|
|
589
569
|
// get_currently_playing
|
|
590
|
-
|
|
591
|
-
this.stateManager.tools_add({
|
|
570
|
+
this.stateManager.tools_upsertBuiltin({
|
|
592
571
|
id: crypto.randomUUID(),
|
|
593
572
|
provider_id: "spotify",
|
|
594
573
|
name: "get_currently_playing",
|
|
@@ -605,12 +584,10 @@ export class Processor {
|
|
|
605
584
|
enabled: true,
|
|
606
585
|
created_at: now,
|
|
607
586
|
max_calls_per_interaction: 3,
|
|
608
|
-
|
|
609
|
-
}
|
|
587
|
+
});
|
|
610
588
|
|
|
611
589
|
// get_liked_songs
|
|
612
|
-
|
|
613
|
-
this.stateManager.tools_add({
|
|
590
|
+
this.stateManager.tools_upsertBuiltin({
|
|
614
591
|
id: crypto.randomUUID(),
|
|
615
592
|
provider_id: "spotify",
|
|
616
593
|
name: "get_liked_songs",
|
|
@@ -627,14 +604,12 @@ export class Processor {
|
|
|
627
604
|
enabled: true,
|
|
628
605
|
created_at: now,
|
|
629
606
|
max_calls_per_interaction: 1,
|
|
630
|
-
|
|
631
|
-
}
|
|
607
|
+
});
|
|
632
608
|
|
|
633
609
|
// submit_response tool — auto-injected for HandlePersonaResponse and HandleRoomResponse.
|
|
634
610
|
// Not user-configurable; invisible in the tools UI. Terminates the tool loop immediately
|
|
635
611
|
// when called; its arguments become response.parsed.
|
|
636
|
-
|
|
637
|
-
this.stateManager.tools_add({
|
|
612
|
+
this.stateManager.tools_upsertBuiltin({
|
|
638
613
|
id: crypto.randomUUID(),
|
|
639
614
|
provider_id: "ei",
|
|
640
615
|
name: "submit_response",
|
|
@@ -653,7 +628,7 @@ export class Processor {
|
|
|
653
628
|
},
|
|
654
629
|
action_response: {
|
|
655
630
|
type: "string",
|
|
656
|
-
description: "
|
|
631
|
+
description: "Italicized stage directions only — physical actions, expressions, or internal states. Keep this distinct from verbal_response: do not repeat or paraphrase what you are saying. If you have nothing to physically do, omit this field.",
|
|
657
632
|
},
|
|
658
633
|
reason: {
|
|
659
634
|
type: "string",
|
|
@@ -669,11 +644,9 @@ export class Processor {
|
|
|
669
644
|
is_submit: true,
|
|
670
645
|
max_calls_per_interaction: 1,
|
|
671
646
|
created_at: now,
|
|
672
|
-
|
|
673
|
-
}
|
|
647
|
+
});
|
|
674
648
|
|
|
675
|
-
|
|
676
|
-
this.stateManager.tools_add({
|
|
649
|
+
this.stateManager.tools_upsertBuiltin({
|
|
677
650
|
id: crypto.randomUUID(),
|
|
678
651
|
provider_id: "ei",
|
|
679
652
|
name: "submit_heartbeat_check",
|
|
@@ -695,11 +668,9 @@ export class Processor {
|
|
|
695
668
|
is_submit: true,
|
|
696
669
|
max_calls_per_interaction: 1,
|
|
697
670
|
created_at: now,
|
|
698
|
-
|
|
699
|
-
}
|
|
671
|
+
});
|
|
700
672
|
|
|
701
|
-
|
|
702
|
-
this.stateManager.tools_add({
|
|
673
|
+
this.stateManager.tools_upsertBuiltin({
|
|
703
674
|
id: crypto.randomUUID(),
|
|
704
675
|
provider_id: "ei",
|
|
705
676
|
name: "submit_ei_heartbeat",
|
|
@@ -721,11 +692,9 @@ export class Processor {
|
|
|
721
692
|
is_submit: true,
|
|
722
693
|
max_calls_per_interaction: 1,
|
|
723
694
|
created_at: now,
|
|
724
|
-
|
|
725
|
-
}
|
|
695
|
+
});
|
|
726
696
|
|
|
727
|
-
|
|
728
|
-
this.stateManager.tools_add({
|
|
697
|
+
this.stateManager.tools_upsertBuiltin({
|
|
729
698
|
id: crypto.randomUUID(),
|
|
730
699
|
provider_id: "ei",
|
|
731
700
|
name: "submit_dedup_decisions",
|
|
@@ -801,8 +770,7 @@ export class Processor {
|
|
|
801
770
|
is_submit: true,
|
|
802
771
|
max_calls_per_interaction: 1,
|
|
803
772
|
created_at: now,
|
|
804
|
-
|
|
805
|
-
}
|
|
773
|
+
});
|
|
806
774
|
}
|
|
807
775
|
|
|
808
776
|
/**
|
|
@@ -223,11 +223,23 @@ export async function buildRoomResponsePromptData(
|
|
|
223
223
|
isTUI: boolean,
|
|
224
224
|
useAllMessages = false
|
|
225
225
|
): Promise<PromptOutput> {
|
|
226
|
+
const MIN_ROOM_MESSAGES = 20;
|
|
227
|
+
|
|
226
228
|
const human = sm.getHuman();
|
|
227
229
|
const activePath = sm.getRoomActivePath(room.id);
|
|
228
|
-
const
|
|
230
|
+
const allSourceMessages = useAllMessages
|
|
229
231
|
? [...sm.getRoomMessages(room.id)].sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
230
232
|
: activePath;
|
|
233
|
+
|
|
234
|
+
// Apply time window (same hours setting as 1:1 personas), but guarantee
|
|
235
|
+
// at least MIN_ROOM_MESSAGES so rooms never feel like they're starting over.
|
|
236
|
+
// Whichever anchor reaches further back wins.
|
|
237
|
+
const contextWindowHours = human.settings?.default_context_window_hours ?? 8;
|
|
238
|
+
const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
|
|
239
|
+
const byTime = allSourceMessages.filter(m => m.timestamp >= windowCutoff);
|
|
240
|
+
const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
|
|
241
|
+
const sourceMessages = byTime.length >= byCount.length ? byTime : byCount;
|
|
242
|
+
|
|
231
243
|
const lastMessage = sourceMessages[sourceMessages.length - 1];
|
|
232
244
|
const currentMessage = lastMessage?.verbal_response;
|
|
233
245
|
|
|
@@ -873,6 +873,17 @@ export class StateManager {
|
|
|
873
873
|
this.scheduleSave();
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
+
tools_upsertBuiltin(tool: ToolDefinition): void {
|
|
877
|
+
const existing = this.tools.find(t => t.name === tool.name);
|
|
878
|
+
if (!existing) {
|
|
879
|
+
this.tools.push(tool);
|
|
880
|
+
} else if (existing.builtin) {
|
|
881
|
+
const idx = this.tools.indexOf(existing);
|
|
882
|
+
this.tools[idx] = { ...tool, id: existing.id, enabled: existing.enabled, created_at: existing.created_at };
|
|
883
|
+
}
|
|
884
|
+
this.scheduleSave();
|
|
885
|
+
}
|
|
886
|
+
|
|
876
887
|
tools_update(id: string, updates: Partial<ToolDefinition>): boolean {
|
|
877
888
|
const idx = this.tools.findIndex(t => t.id === id);
|
|
878
889
|
if (idx === -1) return false;
|
|
@@ -47,7 +47,7 @@ interface EditablePersonaData {
|
|
|
47
47
|
aliases?: string[];
|
|
48
48
|
short_description?: string;
|
|
49
49
|
long_description?: string;
|
|
50
|
-
model?: string;
|
|
50
|
+
model?: string | null;
|
|
51
51
|
group_primary?: string | null;
|
|
52
52
|
groups_visible?: Record<string, boolean>[];
|
|
53
53
|
traits: YAMLTrait[];
|
|
@@ -243,7 +243,7 @@ export function newPersonaFromYAML(yamlContent: string, allTools?: ToolDefinitio
|
|
|
243
243
|
|
|
244
244
|
return {
|
|
245
245
|
long_description: stripPlaceholder(data.long_description, PLACEHOLDER_LONG_DESC),
|
|
246
|
-
model: data.model,
|
|
246
|
+
model: data.model ?? undefined,
|
|
247
247
|
group_primary: data.group_primary ?? "General",
|
|
248
248
|
groups_visible: groupsVisible.length > 0 ? groupsVisible : ["General"],
|
|
249
249
|
traits,
|
|
@@ -276,7 +276,7 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allT
|
|
|
276
276
|
aliases: persona.aliases,
|
|
277
277
|
short_description: persona.short_description,
|
|
278
278
|
long_description: persona.long_description || PLACEHOLDER_LONG_DESC,
|
|
279
|
-
model: modelDisplay,
|
|
279
|
+
model: modelDisplay ?? null,
|
|
280
280
|
group_primary: persona.group_primary,
|
|
281
281
|
groups_visible: groupsForYAML,
|
|
282
282
|
traits: useTraitPlaceholder
|
|
@@ -381,7 +381,7 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
|
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
let resolvedModel: string | undefined = data.model;
|
|
384
|
+
let resolvedModel: string | undefined = data.model ?? undefined;
|
|
385
385
|
if (data.model && accounts && accounts.length > 0) {
|
|
386
386
|
const guid = displayToModelGuid(data.model, accounts);
|
|
387
387
|
if (guid !== undefined) {
|
|
@@ -1220,6 +1220,9 @@ export function toolkitToYAML(provider: ToolProvider, tools: ToolDefinition[]):
|
|
|
1220
1220
|
const toolsMap = tools.length > 0
|
|
1221
1221
|
? Object.fromEntries(tools.map(t => [t.display_name, t.enabled]))
|
|
1222
1222
|
: undefined;
|
|
1223
|
+
if (provider.builtin) {
|
|
1224
|
+
return YAML.stringify({ enabled: provider.enabled, tools: toolsMap }, { lineWidth: 0 });
|
|
1225
|
+
}
|
|
1223
1226
|
const data: EditableToolkitData = {
|
|
1224
1227
|
display_name: provider.display_name,
|
|
1225
1228
|
enabled: provider.enabled,
|
|
@@ -1241,7 +1244,8 @@ export function toolkitFromYAML(yamlContent: string, original: ToolProvider, too
|
|
|
1241
1244
|
const data = YAML.parse(yamlContent) as EditableToolkitData;
|
|
1242
1245
|
|
|
1243
1246
|
if (!data.display_name) {
|
|
1244
|
-
throw new Error("display_name is required");
|
|
1247
|
+
if (!original.display_name) throw new Error("display_name is required");
|
|
1248
|
+
data.display_name = original.display_name;
|
|
1245
1249
|
}
|
|
1246
1250
|
|
|
1247
1251
|
const updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>> = {
|