ei-tui 0.5.2 → 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 +1 -1
- package/src/core/llm-client.ts +63 -1
- package/src/core/processor.ts +60 -68
- package/src/core/prompt-context-builder.ts +18 -11
- package/src/core/room-manager.ts +8 -2
- package/src/core/state-manager.ts +11 -0
- package/src/prompts/response/index.ts +8 -2
- package/src/prompts/response/types.ts +2 -0
- package/src/prompts/room/index.ts +8 -2
- package/src/prompts/room/sections.ts +16 -0
- package/src/prompts/room/types.ts +3 -2
- package/tui/src/components/MessageList.tsx +1 -0
- package/tui/src/components/RoomMessageList.tsx +1 -0
- package/tui/src/util/yaml-serializers.ts +9 -5
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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type ToolProvider,
|
|
25
25
|
} from "./types.js";
|
|
26
26
|
import { buildPersonaFromPersonPrompt } from "../prompts/index.js";
|
|
27
|
+
import { buildSiblingAwarenessSection } from "../prompts/room/index.js";
|
|
27
28
|
import type { PersonaGenerationResult } from "../prompts/generation/types.js";
|
|
28
29
|
|
|
29
30
|
import type { Storage } from "../storage/interface.js";
|
|
@@ -300,18 +301,17 @@ export class Processor {
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
// read_memory tool
|
|
303
|
-
|
|
304
|
-
this.stateManager.tools_add({
|
|
304
|
+
this.stateManager.tools_upsertBuiltin({
|
|
305
305
|
id: crypto.randomUUID(),
|
|
306
306
|
provider_id: "ei",
|
|
307
307
|
name: "read_memory",
|
|
308
308
|
display_name: "Read Memory",
|
|
309
309
|
description:
|
|
310
|
-
"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.",
|
|
311
311
|
input_schema: {
|
|
312
312
|
type: "object",
|
|
313
313
|
properties: {
|
|
314
|
-
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" },
|
|
315
315
|
types: {
|
|
316
316
|
type: "array",
|
|
317
317
|
items: { type: "string", enum: ["fact", "topic", "person", "quote"] },
|
|
@@ -327,12 +327,10 @@ export class Processor {
|
|
|
327
327
|
enabled: true,
|
|
328
328
|
created_at: now,
|
|
329
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).
|
|
330
|
-
|
|
331
|
-
}
|
|
330
|
+
});
|
|
332
331
|
|
|
333
332
|
// file_read tool (TUI only)
|
|
334
|
-
|
|
335
|
-
this.stateManager.tools_add({
|
|
333
|
+
this.stateManager.tools_upsertBuiltin({
|
|
336
334
|
id: crypto.randomUUID(),
|
|
337
335
|
provider_id: "ei",
|
|
338
336
|
name: "file_read",
|
|
@@ -351,12 +349,10 @@ export class Processor {
|
|
|
351
349
|
enabled: true,
|
|
352
350
|
created_at: now,
|
|
353
351
|
max_calls_per_interaction: 5,
|
|
354
|
-
|
|
355
|
-
}
|
|
352
|
+
});
|
|
356
353
|
|
|
357
354
|
// list_directory tool (TUI only)
|
|
358
|
-
|
|
359
|
-
this.stateManager.tools_add({
|
|
355
|
+
this.stateManager.tools_upsertBuiltin({
|
|
360
356
|
id: crypto.randomUUID(),
|
|
361
357
|
provider_id: "ei",
|
|
362
358
|
name: "list_directory",
|
|
@@ -375,12 +371,10 @@ export class Processor {
|
|
|
375
371
|
enabled: true,
|
|
376
372
|
created_at: now,
|
|
377
373
|
max_calls_per_interaction: 5,
|
|
378
|
-
|
|
379
|
-
}
|
|
374
|
+
});
|
|
380
375
|
|
|
381
376
|
// directory_tree tool (TUI only)
|
|
382
|
-
|
|
383
|
-
this.stateManager.tools_add({
|
|
377
|
+
this.stateManager.tools_upsertBuiltin({
|
|
384
378
|
id: crypto.randomUUID(),
|
|
385
379
|
provider_id: "ei",
|
|
386
380
|
name: "directory_tree",
|
|
@@ -400,12 +394,10 @@ export class Processor {
|
|
|
400
394
|
enabled: true,
|
|
401
395
|
created_at: now,
|
|
402
396
|
max_calls_per_interaction: 3,
|
|
403
|
-
|
|
404
|
-
}
|
|
397
|
+
});
|
|
405
398
|
|
|
406
399
|
// search_files tool (TUI only)
|
|
407
|
-
|
|
408
|
-
this.stateManager.tools_add({
|
|
400
|
+
this.stateManager.tools_upsertBuiltin({
|
|
409
401
|
id: crypto.randomUUID(),
|
|
410
402
|
provider_id: "ei",
|
|
411
403
|
name: "search_files",
|
|
@@ -425,12 +417,10 @@ export class Processor {
|
|
|
425
417
|
enabled: true,
|
|
426
418
|
created_at: now,
|
|
427
419
|
max_calls_per_interaction: 3,
|
|
428
|
-
|
|
429
|
-
}
|
|
420
|
+
});
|
|
430
421
|
|
|
431
422
|
// grep tool (TUI only)
|
|
432
|
-
|
|
433
|
-
this.stateManager.tools_add({
|
|
423
|
+
this.stateManager.tools_upsertBuiltin({
|
|
434
424
|
id: crypto.randomUUID(),
|
|
435
425
|
provider_id: "ei",
|
|
436
426
|
name: "grep",
|
|
@@ -452,12 +442,10 @@ export class Processor {
|
|
|
452
442
|
enabled: true,
|
|
453
443
|
created_at: now,
|
|
454
444
|
max_calls_per_interaction: 5,
|
|
455
|
-
|
|
456
|
-
}
|
|
445
|
+
});
|
|
457
446
|
|
|
458
447
|
// get_file_info tool (TUI only)
|
|
459
|
-
|
|
460
|
-
this.stateManager.tools_add({
|
|
448
|
+
this.stateManager.tools_upsertBuiltin({
|
|
461
449
|
id: crypto.randomUUID(),
|
|
462
450
|
provider_id: "ei",
|
|
463
451
|
name: "get_file_info",
|
|
@@ -476,12 +464,10 @@ export class Processor {
|
|
|
476
464
|
enabled: true,
|
|
477
465
|
created_at: now,
|
|
478
466
|
max_calls_per_interaction: 5,
|
|
479
|
-
|
|
480
|
-
}
|
|
467
|
+
});
|
|
481
468
|
|
|
482
469
|
// web_fetch tool
|
|
483
|
-
|
|
484
|
-
this.stateManager.tools_add({
|
|
470
|
+
this.stateManager.tools_upsertBuiltin({
|
|
485
471
|
id: crypto.randomUUID(),
|
|
486
472
|
provider_id: "ei",
|
|
487
473
|
name: "web_fetch",
|
|
@@ -500,8 +486,7 @@ export class Processor {
|
|
|
500
486
|
enabled: true,
|
|
501
487
|
created_at: now,
|
|
502
488
|
max_calls_per_interaction: 3,
|
|
503
|
-
|
|
504
|
-
}
|
|
489
|
+
});
|
|
505
490
|
|
|
506
491
|
// --- Tavily Search provider ---
|
|
507
492
|
if (!this.stateManager.tools_getProviderById("tavily")) {
|
|
@@ -520,8 +505,7 @@ export class Processor {
|
|
|
520
505
|
}
|
|
521
506
|
|
|
522
507
|
// tavily_web_search
|
|
523
|
-
|
|
524
|
-
this.stateManager.tools_add({
|
|
508
|
+
this.stateManager.tools_upsertBuiltin({
|
|
525
509
|
id: crypto.randomUUID(),
|
|
526
510
|
provider_id: "tavily",
|
|
527
511
|
name: "tavily_web_search",
|
|
@@ -541,12 +525,10 @@ export class Processor {
|
|
|
541
525
|
enabled: true,
|
|
542
526
|
created_at: now,
|
|
543
527
|
max_calls_per_interaction: 3,
|
|
544
|
-
|
|
545
|
-
}
|
|
528
|
+
});
|
|
546
529
|
|
|
547
530
|
// tavily_news_search
|
|
548
|
-
|
|
549
|
-
this.stateManager.tools_add({
|
|
531
|
+
this.stateManager.tools_upsertBuiltin({
|
|
550
532
|
id: crypto.randomUUID(),
|
|
551
533
|
provider_id: "tavily",
|
|
552
534
|
name: "tavily_news_search",
|
|
@@ -566,8 +548,7 @@ export class Processor {
|
|
|
566
548
|
enabled: true,
|
|
567
549
|
created_at: now,
|
|
568
550
|
max_calls_per_interaction: 3,
|
|
569
|
-
|
|
570
|
-
}
|
|
551
|
+
});
|
|
571
552
|
|
|
572
553
|
// --- Spotify provider ---
|
|
573
554
|
if (!this.stateManager.tools_getProviderById("spotify")) {
|
|
@@ -586,8 +567,7 @@ export class Processor {
|
|
|
586
567
|
}
|
|
587
568
|
|
|
588
569
|
// get_currently_playing
|
|
589
|
-
|
|
590
|
-
this.stateManager.tools_add({
|
|
570
|
+
this.stateManager.tools_upsertBuiltin({
|
|
591
571
|
id: crypto.randomUUID(),
|
|
592
572
|
provider_id: "spotify",
|
|
593
573
|
name: "get_currently_playing",
|
|
@@ -604,12 +584,10 @@ export class Processor {
|
|
|
604
584
|
enabled: true,
|
|
605
585
|
created_at: now,
|
|
606
586
|
max_calls_per_interaction: 3,
|
|
607
|
-
|
|
608
|
-
}
|
|
587
|
+
});
|
|
609
588
|
|
|
610
589
|
// get_liked_songs
|
|
611
|
-
|
|
612
|
-
this.stateManager.tools_add({
|
|
590
|
+
this.stateManager.tools_upsertBuiltin({
|
|
613
591
|
id: crypto.randomUUID(),
|
|
614
592
|
provider_id: "spotify",
|
|
615
593
|
name: "get_liked_songs",
|
|
@@ -626,14 +604,12 @@ export class Processor {
|
|
|
626
604
|
enabled: true,
|
|
627
605
|
created_at: now,
|
|
628
606
|
max_calls_per_interaction: 1,
|
|
629
|
-
|
|
630
|
-
}
|
|
607
|
+
});
|
|
631
608
|
|
|
632
609
|
// submit_response tool — auto-injected for HandlePersonaResponse and HandleRoomResponse.
|
|
633
610
|
// Not user-configurable; invisible in the tools UI. Terminates the tool loop immediately
|
|
634
611
|
// when called; its arguments become response.parsed.
|
|
635
|
-
|
|
636
|
-
this.stateManager.tools_add({
|
|
612
|
+
this.stateManager.tools_upsertBuiltin({
|
|
637
613
|
id: crypto.randomUUID(),
|
|
638
614
|
provider_id: "ei",
|
|
639
615
|
name: "submit_response",
|
|
@@ -652,7 +628,7 @@ export class Processor {
|
|
|
652
628
|
},
|
|
653
629
|
action_response: {
|
|
654
630
|
type: "string",
|
|
655
|
-
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.",
|
|
656
632
|
},
|
|
657
633
|
reason: {
|
|
658
634
|
type: "string",
|
|
@@ -668,11 +644,9 @@ export class Processor {
|
|
|
668
644
|
is_submit: true,
|
|
669
645
|
max_calls_per_interaction: 1,
|
|
670
646
|
created_at: now,
|
|
671
|
-
|
|
672
|
-
}
|
|
647
|
+
});
|
|
673
648
|
|
|
674
|
-
|
|
675
|
-
this.stateManager.tools_add({
|
|
649
|
+
this.stateManager.tools_upsertBuiltin({
|
|
676
650
|
id: crypto.randomUUID(),
|
|
677
651
|
provider_id: "ei",
|
|
678
652
|
name: "submit_heartbeat_check",
|
|
@@ -694,11 +668,9 @@ export class Processor {
|
|
|
694
668
|
is_submit: true,
|
|
695
669
|
max_calls_per_interaction: 1,
|
|
696
670
|
created_at: now,
|
|
697
|
-
|
|
698
|
-
}
|
|
671
|
+
});
|
|
699
672
|
|
|
700
|
-
|
|
701
|
-
this.stateManager.tools_add({
|
|
673
|
+
this.stateManager.tools_upsertBuiltin({
|
|
702
674
|
id: crypto.randomUUID(),
|
|
703
675
|
provider_id: "ei",
|
|
704
676
|
name: "submit_ei_heartbeat",
|
|
@@ -720,11 +692,9 @@ export class Processor {
|
|
|
720
692
|
is_submit: true,
|
|
721
693
|
max_calls_per_interaction: 1,
|
|
722
694
|
created_at: now,
|
|
723
|
-
|
|
724
|
-
}
|
|
695
|
+
});
|
|
725
696
|
|
|
726
|
-
|
|
727
|
-
this.stateManager.tools_add({
|
|
697
|
+
this.stateManager.tools_upsertBuiltin({
|
|
728
698
|
id: crypto.randomUUID(),
|
|
729
699
|
provider_id: "ei",
|
|
730
700
|
name: "submit_dedup_decisions",
|
|
@@ -800,8 +770,7 @@ export class Processor {
|
|
|
800
770
|
is_submit: true,
|
|
801
771
|
max_calls_per_interaction: 1,
|
|
802
772
|
created_at: now,
|
|
803
|
-
|
|
804
|
-
}
|
|
773
|
+
});
|
|
805
774
|
}
|
|
806
775
|
|
|
807
776
|
/**
|
|
@@ -1024,8 +993,9 @@ export class Processor {
|
|
|
1024
993
|
const isBackingOff = retryAfter !== null && retryAfter > new Date().toISOString();
|
|
1025
994
|
|
|
1026
995
|
if (!isBackingOff) {
|
|
1027
|
-
|
|
996
|
+
let request = this.stateManager.queue_claimHighest();
|
|
1028
997
|
if (request) {
|
|
998
|
+
request = this.augmentRoomRequest(request);
|
|
1029
999
|
const personaId = request.data.personaId as string | undefined;
|
|
1030
1000
|
const personaDisplayName = request.data.personaDisplayName as string | undefined;
|
|
1031
1001
|
const personaSuffix = personaDisplayName ? ` [${personaDisplayName}]` : "";
|
|
@@ -1412,6 +1382,28 @@ const toolNextSteps = new Set([
|
|
|
1412
1382
|
});
|
|
1413
1383
|
}
|
|
1414
1384
|
|
|
1385
|
+
private augmentRoomRequest(request: LLMRequest): LLMRequest {
|
|
1386
|
+
if (request.next_step !== LLMNextStep.HandleRoomResponse) return request;
|
|
1387
|
+
|
|
1388
|
+
const roomId = request.data.roomId as string | undefined;
|
|
1389
|
+
const parentMessageId = request.data.parentMessageId as string | undefined;
|
|
1390
|
+
const personaDisplayName = request.data.personaDisplayName as string | undefined;
|
|
1391
|
+
|
|
1392
|
+
if (!roomId || !parentMessageId || !personaDisplayName) return request;
|
|
1393
|
+
|
|
1394
|
+
const siblings = this.stateManager.getRoomChildren(roomId, parentMessageId)
|
|
1395
|
+
.filter((m: RoomMessage) => m.role === "persona" && m.verbal_response)
|
|
1396
|
+
.map((m: RoomMessage) => ({
|
|
1397
|
+
name: this.stateManager.persona_getById(m.persona_id ?? "")?.display_name ?? "Participant",
|
|
1398
|
+
verbal_response: m.verbal_response!,
|
|
1399
|
+
}));
|
|
1400
|
+
|
|
1401
|
+
if (siblings.length === 0) return request;
|
|
1402
|
+
|
|
1403
|
+
const siblingSection = buildSiblingAwarenessSection(siblings, personaDisplayName);
|
|
1404
|
+
return { ...request, system: request.system + "\n\n" + siblingSection };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1415
1407
|
private classifyLLMError(error: string): string {
|
|
1416
1408
|
const match = error.match(/\((\d{3})\)/);
|
|
1417
1409
|
if (match) {
|
|
@@ -3,7 +3,8 @@ import { StateManager } from "./state-manager.js";
|
|
|
3
3
|
import { getEmbeddingService, findTopK } from "./embedding-service.js";
|
|
4
4
|
import type { ResponsePromptData, PromptOutput } from "../prompts/index.js";
|
|
5
5
|
import { buildRoomResponsePrompt } from "../prompts/room/index.js";
|
|
6
|
-
import type { RoomParticipantIdentity
|
|
6
|
+
import type { RoomParticipantIdentity } from "../prompts/room/types.js";
|
|
7
|
+
import { normalizeRoomMessages } from "./handlers/utils.js";
|
|
7
8
|
|
|
8
9
|
const QUOTE_LIMIT = 10;
|
|
9
10
|
const DATA_ITEM_LIMIT = 15;
|
|
@@ -205,6 +206,7 @@ export async function buildResponsePromptData(
|
|
|
205
206
|
traits: persona.traits,
|
|
206
207
|
topics: persona.topics,
|
|
207
208
|
interested_topics: persona.topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
|
|
209
|
+
include_message_timestamps: persona.include_message_timestamps,
|
|
208
210
|
},
|
|
209
211
|
human: filteredHuman,
|
|
210
212
|
visible_personas: visiblePersonas,
|
|
@@ -221,25 +223,29 @@ export async function buildRoomResponsePromptData(
|
|
|
221
223
|
isTUI: boolean,
|
|
222
224
|
useAllMessages = false
|
|
223
225
|
): Promise<PromptOutput> {
|
|
226
|
+
const MIN_ROOM_MESSAGES = 20;
|
|
227
|
+
|
|
224
228
|
const human = sm.getHuman();
|
|
225
229
|
const activePath = sm.getRoomActivePath(room.id);
|
|
226
|
-
const
|
|
230
|
+
const allSourceMessages = useAllMessages
|
|
227
231
|
? [...sm.getRoomMessages(room.id)].sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
228
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
|
+
|
|
229
243
|
const lastMessage = sourceMessages[sourceMessages.length - 1];
|
|
230
244
|
const currentMessage = lastMessage?.verbal_response;
|
|
231
245
|
|
|
232
246
|
const filteredHuman = await filterHumanDataByVisibility(human, respondingPersona, currentMessage);
|
|
233
247
|
|
|
234
|
-
const history
|
|
235
|
-
speaker_name: m.role === "human"
|
|
236
|
-
? (human.settings?.name_display ?? "Human")
|
|
237
|
-
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? m.persona_id ?? "Unknown"),
|
|
238
|
-
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
239
|
-
verbal_response: m.verbal_response,
|
|
240
|
-
action_response: m.action_response,
|
|
241
|
-
silence_reason: m.silence_reason,
|
|
242
|
-
}));
|
|
248
|
+
const history = normalizeRoomMessages(sourceMessages, sm);
|
|
243
249
|
|
|
244
250
|
const otherParticipants: RoomParticipantIdentity[] = [];
|
|
245
251
|
for (const pid of room.persona_ids) {
|
|
@@ -273,6 +279,7 @@ export async function buildRoomResponsePromptData(
|
|
|
273
279
|
long_description: respondingPersona.long_description,
|
|
274
280
|
traits: respondingPersona.traits,
|
|
275
281
|
topics: respondingPersona.topics,
|
|
282
|
+
include_message_timestamps: respondingPersona.include_message_timestamps,
|
|
276
283
|
},
|
|
277
284
|
other_participants: otherParticipants,
|
|
278
285
|
human: filteredHuman,
|
package/src/core/room-manager.ts
CHANGED
|
@@ -32,7 +32,11 @@ async function queueRoomPersonaResponses(
|
|
|
32
32
|
isTUI: boolean,
|
|
33
33
|
onRoomMessageQueued: (roomId: string) => void
|
|
34
34
|
): Promise<void> {
|
|
35
|
-
|
|
35
|
+
const personaIds = room.mode === RoomMode.FreeForAll
|
|
36
|
+
? [...room.persona_ids].sort(() => Math.random() - 0.5)
|
|
37
|
+
: room.persona_ids;
|
|
38
|
+
|
|
39
|
+
for (const personaId of personaIds) {
|
|
36
40
|
const persona = sm.persona_getById(personaId);
|
|
37
41
|
if (!persona || persona.is_archived || persona.is_paused) continue;
|
|
38
42
|
if (room.mode === RoomMode.MessagesAgainstPersona && room.judge_persona_id === personaId) continue;
|
|
@@ -186,7 +190,9 @@ export async function sendFfaMessage(
|
|
|
186
190
|
.map(q => q.data.personaId as string)
|
|
187
191
|
);
|
|
188
192
|
|
|
189
|
-
|
|
193
|
+
const shuffledIds = [...updatedRoom.persona_ids].sort(() => Math.random() - 0.5);
|
|
194
|
+
|
|
195
|
+
for (const personaId of shuffledIds) {
|
|
190
196
|
if (alreadyQueued.has(personaId)) continue;
|
|
191
197
|
const persona = sm.persona_getById(personaId);
|
|
192
198
|
if (!persona || persona.is_archived || persona.is_paused) continue;
|
|
@@ -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;
|
|
@@ -54,6 +54,9 @@ Your role is unique among personas:
|
|
|
54
54
|
const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
|
|
55
55
|
const currentTime = formatCurrentTime();
|
|
56
56
|
const conversationState = getConversationStateText(data.delay_ms);
|
|
57
|
+
const timestampNote = data.persona.include_message_timestamps
|
|
58
|
+
? `\nNote: Timestamps are shown to help you understand time context — the user sees them too, no need to echo or reference them.`
|
|
59
|
+
: "";
|
|
57
60
|
|
|
58
61
|
return `${identity}
|
|
59
62
|
|
|
@@ -71,7 +74,7 @@ ${priorities}
|
|
|
71
74
|
|
|
72
75
|
${responseFormat}${toolsSection ? `\n\n${toolsSection}` : ""}
|
|
73
76
|
|
|
74
|
-
Current time: ${currentTime}
|
|
77
|
+
Current time: ${currentTime}${timestampNote}
|
|
75
78
|
${conversationState}
|
|
76
79
|
|
|
77
80
|
## Final Instructions
|
|
@@ -99,6 +102,9 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
|
99
102
|
const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
|
|
100
103
|
const currentTime = formatCurrentTime();
|
|
101
104
|
const conversationState = getConversationStateText(data.delay_ms);
|
|
105
|
+
const timestampNote = data.persona.include_message_timestamps
|
|
106
|
+
? `\nNote: Timestamps are shown to help you understand time context — the user sees them too, no need to echo or reference them.`
|
|
107
|
+
: "";
|
|
102
108
|
|
|
103
109
|
return `${identity}
|
|
104
110
|
|
|
@@ -115,7 +121,7 @@ ${priorities}
|
|
|
115
121
|
|
|
116
122
|
${responseFormat}${toolsSection ? `\n\n${toolsSection}` : ""}
|
|
117
123
|
|
|
118
|
-
Current time: ${currentTime}
|
|
124
|
+
Current time: ${currentTime}${timestampNote}
|
|
119
125
|
${conversationState}
|
|
120
126
|
|
|
121
127
|
## Final Instructions
|
|
@@ -19,6 +19,8 @@ export interface ResponsePromptData {
|
|
|
19
19
|
topics: PersonaTopic[];
|
|
20
20
|
/** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
|
|
21
21
|
interested_topics: PersonaTopic[];
|
|
22
|
+
/** When true, each message has a timestamp prepended; include a note so the persona doesn't echo them */
|
|
23
|
+
include_message_timestamps?: boolean;
|
|
22
24
|
};
|
|
23
25
|
human: {
|
|
24
26
|
facts: Fact[];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { RoomResponsePromptData, RoomJudgePromptData, PromptOutput } from "./types.js";
|
|
6
6
|
import { formatCurrentTime } from "../../core/format-utils.js";
|
|
7
|
+
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
7
8
|
import {
|
|
8
9
|
buildRoomParticipantsSection,
|
|
9
10
|
buildRoomHistorySection,
|
|
@@ -14,6 +15,8 @@ import {
|
|
|
14
15
|
buildJudgeCandidatesSection,
|
|
15
16
|
buildJudgeDecisionFormatSection,
|
|
16
17
|
} from "./sections.js";
|
|
18
|
+
|
|
19
|
+
export { buildSiblingAwarenessSection } from "./sections.js";
|
|
17
20
|
import {
|
|
18
21
|
buildHumanSection,
|
|
19
22
|
buildQuotesSection,
|
|
@@ -48,6 +51,9 @@ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOut
|
|
|
48
51
|
const responseFormat = buildRoomResponseFormatSection();
|
|
49
52
|
const toolsSection = tools && tools.length > 0 ? buildToolsSection() : "";
|
|
50
53
|
const currentTime = formatCurrentTime();
|
|
54
|
+
const timestampNote = persona.include_message_timestamps
|
|
55
|
+
? `Note: Timestamps are shown to help you understand time context — the user sees them too, no need to echo or reference them.`
|
|
56
|
+
: "";
|
|
51
57
|
|
|
52
58
|
const system = [
|
|
53
59
|
identity,
|
|
@@ -61,10 +67,10 @@ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOut
|
|
|
61
67
|
guidelines,
|
|
62
68
|
responseFormat,
|
|
63
69
|
toolsSection,
|
|
64
|
-
`Current time: ${currentTime}`,
|
|
70
|
+
`Current time: ${currentTime}${timestampNote ? `\n${timestampNote}` : ""}`,
|
|
65
71
|
].filter(Boolean).join("\n\n");
|
|
66
72
|
|
|
67
|
-
const user =
|
|
73
|
+
const user = formatMessagesAsPlaceholders(history, name) +
|
|
68
74
|
`\n\nRespond to the conversation above as ${name}. Call the \`submit_response\` tool with your response. If the tool is unavailable, use the JSON format in the Response Format section.`;
|
|
69
75
|
|
|
70
76
|
return { system, user };
|
|
@@ -123,6 +123,22 @@ Rules:
|
|
|
123
123
|
- If the \`submit_response\` tool is unavailable, return the JSON object directly as your entire reply — no prose, no preamble`;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export function buildSiblingAwarenessSection(
|
|
127
|
+
siblings: Array<{ name: string; verbal_response: string }>,
|
|
128
|
+
personaName: string
|
|
129
|
+
): string {
|
|
130
|
+
if (siblings.length === 0) return "";
|
|
131
|
+
const lines = siblings.map(s => `**${s.name}**: "${s.verbal_response}"`);
|
|
132
|
+
const header = siblings.length === 1
|
|
133
|
+
? "## Another voice has already responded this round"
|
|
134
|
+
: "## Others have already responded this round";
|
|
135
|
+
return `${header}
|
|
136
|
+
|
|
137
|
+
${lines.join("\n\n")}
|
|
138
|
+
|
|
139
|
+
Find the angle that's distinctly yours on this same moment — don't try to cover more ground, just be the version of this reaction that only *${personaName}* could give.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
export function buildJudgeCandidatesSection(candidates: RoomJudgeCandidate[]): string {
|
|
127
143
|
const lines = candidates.map((c, i) => {
|
|
128
144
|
const speaker = c.speaker_id === "human" ? "Human" : c.speaker_name;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Room Prompt Types
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic, ToolDefinition } from "../../core/types.js";
|
|
5
|
+
import type { Fact, Message, PersonaTrait, Topic, Person, Quote, PersonaTopic, ToolDefinition } from "../../core/types.js";
|
|
6
6
|
import type { RoomMode } from "../../core/types.js";
|
|
7
7
|
|
|
8
8
|
export interface RoomParticipantIdentity {
|
|
@@ -35,6 +35,7 @@ export interface RoomResponsePromptData {
|
|
|
35
35
|
long_description?: string;
|
|
36
36
|
traits: PersonaTrait[];
|
|
37
37
|
topics: PersonaTopic[];
|
|
38
|
+
include_message_timestamps?: boolean;
|
|
38
39
|
};
|
|
39
40
|
other_participants: RoomParticipantIdentity[];
|
|
40
41
|
human: {
|
|
@@ -47,7 +48,7 @@ export interface RoomResponsePromptData {
|
|
|
47
48
|
/** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
|
|
48
49
|
interested_topics: Topic[];
|
|
49
50
|
};
|
|
50
|
-
history:
|
|
51
|
+
history: Message[];
|
|
51
52
|
isTUI: boolean;
|
|
52
53
|
tools?: ToolDefinition[];
|
|
53
54
|
}
|
|
@@ -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'>> = {
|