viberoom 0.5.8 → 0.6.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.
- package/README.md +20 -0
- package/dist/agent-health.js +52 -0
- package/dist/context.js +6 -0
- package/dist/duration.js +12 -0
- package/dist/hub.js +13 -2
- package/dist/launcher.js +19 -2
- package/dist/main.js +77 -34
- package/dist/mcp-skills-server.js +143 -0
- package/dist/persona.js +167 -46
- package/dist/quotes.js +41 -0
- package/dist/recipes.js +28 -1
- package/dist/room-design.js +205 -0
- package/dist/room.js +388 -150
- package/dist/rows.js +19 -0
- package/dist/server.js +304 -27
- package/dist/skills.js +28 -2
- package/dist/templates.js +17 -0
- package/dist/update.js +32 -2
- package/dist/viewer.js +69 -2
- package/dist/ws.js +175 -0
- package/package.json +2 -1
- package/ui/app.css +348 -246
- package/ui/app.js +1422 -345
- package/ui/avatars.js +131 -30
- package/ui/components.css +140 -0
- package/ui/components.js +342 -0
- package/ui/icons.js +11 -0
- package/ui/index.html +58 -36
- package/ui/theme.css +457 -180
- package/ui/tokens.js +620 -0
- package/ui/ui.js +113 -0
package/dist/room.js
CHANGED
|
@@ -3,20 +3,25 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.js";
|
|
6
|
-
import { NOTES_ONLY_PROMPT, NOTES_REQUEST, crossedThreshold, extractNotes, isBareContextFullError, isContextFullError, overThreshold, visibleChunk } from "./context.js";
|
|
6
|
+
import { NOTES_ONLY_PROMPT, NOTES_REQUEST, crossedThreshold, emptyUsageReport, extractNotes, isBareContextFullError, isContextFullError, looksCompacted, overThreshold, visibleChunk } from "./context.js";
|
|
7
|
+
import { formatDuration } from "./duration.js";
|
|
7
8
|
import { affectedByEdit, editNotice, partitionHistory, rewriteNotice } from "./edit.js";
|
|
8
9
|
import { saveImages } from "./files.js";
|
|
10
|
+
import { agentReadableWindow, resolveQuotes } from "./quotes.js";
|
|
9
11
|
import { join, resolve } from "node:path";
|
|
10
12
|
import { AcpAgent } from "./acp-client.js";
|
|
11
13
|
import { RemoteError } from "./jsonrpc.js";
|
|
12
14
|
import { getRecipe, listRecipes } from "./recipes.js";
|
|
15
|
+
import { classifyStartFailure } from "./agent-health.js";
|
|
16
|
+
import { legacyRowTone } from "./rows.js";
|
|
13
17
|
import { composeSkillBlock, skillPull, SKILL_TOOL_NAME } from "./persona.js";
|
|
14
18
|
import { BUILTIN_AUTHOR, parseSkillInvocation, renderSkillBody, SKILL_NAME_PATTERN, } from "./skills.js";
|
|
15
|
-
import {
|
|
19
|
+
import { templateId } from "./templates.js";
|
|
20
|
+
import { applyVibemateChanges, diffSettings, lintRoomDesign, ruleLines } from "./room-design.js";
|
|
21
|
+
import { BRIEF_AFFECTING_SETTINGS, AGENT_SETTINGS, coerceSetting, describeSettings, DEFAULT_ROOM_SETTINGS, ROOM_SETTINGS_SPEC, REQUEST_BRIEF_MARKER, NAME_PATTERN, SILENT_MARKER, buildBrief, buildHeader, composeCorrectionPrompt, composePrompt, countSentences, ensureDir, } from "./persona.js";
|
|
16
22
|
import { Transcript } from "./log.js";
|
|
17
23
|
const SKILL_TOOL_READY_MS = 5000;
|
|
18
24
|
const COLORS = ["#6d5dfc", "#16a34a", "#d97706", "#dc2626", "#0891b2", "#be185d", "#4d7c0f", "#7c3aed"];
|
|
19
|
-
const NAME_PATTERN = /^[\p{L}\p{N}][\p{L}\p{N}_-]{0,23}$/u;
|
|
20
25
|
const MENTION_PATTERN = /@([\p{L}\p{N}][\p{L}\p{N}_-]*)/gu;
|
|
21
26
|
const RULE_REF_TOKEN = /@\{p:([^}]+)\}/g;
|
|
22
27
|
const ADAPTER_ERROR_PATTERN = /^(?:Warning: Falling back from WebSockets|unexpected status \d{3}|Error when talking to|API Error|You have exhausted your (?:daily )?quota|Rate limit|429 |5\d\d )/i;
|
|
@@ -45,6 +50,8 @@ export class Room extends EventEmitter {
|
|
|
45
50
|
runtimes = new Map();
|
|
46
51
|
drafts = new Map();
|
|
47
52
|
permissions = new Map();
|
|
53
|
+
proposals = new Map();
|
|
54
|
+
proposalPlans = new Map();
|
|
48
55
|
optionCache;
|
|
49
56
|
log;
|
|
50
57
|
colorIndex = 0;
|
|
@@ -90,6 +97,11 @@ export class Room extends EventEmitter {
|
|
|
90
97
|
for (const line of lines) {
|
|
91
98
|
try {
|
|
92
99
|
const message = JSON.parse(line);
|
|
100
|
+
if (message.kind === "system" && !message.details?.tone) {
|
|
101
|
+
const tone = legacyRowTone(message.text);
|
|
102
|
+
if (tone)
|
|
103
|
+
message.details = { ...(message.details ?? {}), tone };
|
|
104
|
+
}
|
|
93
105
|
this.messages.push(message);
|
|
94
106
|
if (message.seq > this.seq)
|
|
95
107
|
this.seq = message.seq;
|
|
@@ -209,6 +221,19 @@ export class Room extends EventEmitter {
|
|
|
209
221
|
appendFileSync(this.historyPath(), JSON.stringify(message) + "\n");
|
|
210
222
|
this.push({ type: "message", message });
|
|
211
223
|
}
|
|
224
|
+
messagesWithLiveDrafts() {
|
|
225
|
+
const live = [...this.runtimes.values()].filter((r) => r.turn?.published).map((r) => r.turn.message);
|
|
226
|
+
if (!live.length)
|
|
227
|
+
return [...this.messages];
|
|
228
|
+
const out = [...this.messages];
|
|
229
|
+
for (const draft of live) {
|
|
230
|
+
let at = out.length;
|
|
231
|
+
while (at > 0 && out[at - 1].ts > draft.ts)
|
|
232
|
+
at--;
|
|
233
|
+
out.splice(at, 0, draft);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
212
237
|
get humanName() {
|
|
213
238
|
return this.settings.humanName;
|
|
214
239
|
}
|
|
@@ -229,8 +254,9 @@ export class Room extends EventEmitter {
|
|
|
229
254
|
settings: this.settings,
|
|
230
255
|
customRulesText: this.renderRuleReferences(this.settings.customRules),
|
|
231
256
|
participants: [...this.participants.values()],
|
|
232
|
-
messages:
|
|
257
|
+
messages: this.messagesWithLiveDrafts(),
|
|
233
258
|
permissions: [...this.permissions.values()].map(({ resolve: _r, ...p }) => p),
|
|
259
|
+
proposals: [...this.proposals.values()],
|
|
234
260
|
recipes: listRecipes().map(({ build: _b, ...r }) => r),
|
|
235
261
|
lastMessageAt: last?.ts ?? this.createdAt,
|
|
236
262
|
};
|
|
@@ -292,13 +318,14 @@ export class Room extends EventEmitter {
|
|
|
292
318
|
unstaffed() {
|
|
293
319
|
return [...this.participants.values()].filter((p) => p.kind === "agent" && p.status === "unstaffed");
|
|
294
320
|
}
|
|
295
|
-
postHumanMessage(text, images = []) {
|
|
321
|
+
postHumanMessage(text, images = [], quotes = []) {
|
|
296
322
|
const waiting = this.unstaffed();
|
|
297
323
|
if (waiting.length)
|
|
298
324
|
throw new Error(`${waiting.map((p) => p.name).join(", ")} ${waiting.length === 1 ? "has" : "have"} no coding agent yet: summon ${waiting.length === 1 ? "it" : "them"} from the roster to start the conversation`);
|
|
299
325
|
const trimmed = text.trim();
|
|
300
|
-
if (!trimmed && !images.length)
|
|
326
|
+
if (!trimmed && !images.length && !quotes.length)
|
|
301
327
|
throw new Error("empty message");
|
|
328
|
+
const quoted = quotes.length ? resolveQuotes(quotes, this.messages) : [];
|
|
302
329
|
const attachments = images.length ? saveImages(ensureDir(this.filesDir()), images) : [];
|
|
303
330
|
this.humanTypingUntil = 0;
|
|
304
331
|
const human = this.participants.get("human");
|
|
@@ -315,6 +342,8 @@ export class Room extends EventEmitter {
|
|
|
315
342
|
};
|
|
316
343
|
if (attachments.length)
|
|
317
344
|
message.images = attachments;
|
|
345
|
+
if (quoted.length)
|
|
346
|
+
message.quotes = quoted;
|
|
318
347
|
this.decorateHumanMessage(message);
|
|
319
348
|
human.turns += 1;
|
|
320
349
|
if (this.focused) {
|
|
@@ -466,6 +495,7 @@ export class Room extends EventEmitter {
|
|
|
466
495
|
const memory = !!options.memory;
|
|
467
496
|
const replay = memory ? Math.max(0, options.replay ?? this.settings.replayAfterRestart) : 0;
|
|
468
497
|
const withNotes = memory && !!participant.notes;
|
|
498
|
+
const why = options.reason ?? "its context was cleared";
|
|
469
499
|
this.dropScheduledTurn(id);
|
|
470
500
|
this.cancelPermissionsOf(id);
|
|
471
501
|
if (this.speaking === id)
|
|
@@ -476,18 +506,38 @@ export class Room extends EventEmitter {
|
|
|
476
506
|
this.restoredSeen.set(id, this.seq);
|
|
477
507
|
this.push({ type: "participant", participant });
|
|
478
508
|
if (!online) {
|
|
479
|
-
participant.statusDetail = withNotes ?
|
|
480
|
-
|
|
509
|
+
participant.statusDetail = withNotes ? `${why}; a reconnect starts it with its notes` : `${why}; a reconnect starts it with an empty head`;
|
|
510
|
+
const comesBack = withNotes ? "it comes back with its notes" : "it comes back knowing nothing from before";
|
|
511
|
+
this.postSystem(options.reason ? `${participant.name} was respawned while offline (${why}): ${comesBack}.` : `${participant.name} was respawned while offline: ${comesBack}.`);
|
|
481
512
|
this.push({ type: "participant", participant });
|
|
482
513
|
this.log.info(`respawn of ${participant.name} (offline): stored session dropped`);
|
|
483
514
|
return participant;
|
|
484
515
|
}
|
|
485
516
|
await this.reconnect(id, memory
|
|
486
|
-
? { mode: "replay", replay, memory: withNotes, reason:
|
|
487
|
-
: { mode: "replay", replay: 0, reason:
|
|
517
|
+
? { mode: "replay", replay, memory: withNotes, reason: `${why}; it comes back with ${withNotes ? "its notes and " : ""}the last ${replay} messages` }
|
|
518
|
+
: { mode: "replay", replay: 0, reason: `${why}, it remembers nothing from before` });
|
|
488
519
|
this.log.info(`respawn of ${participant.name}: fresh session, ${memory ? `${withNotes ? "notes + " : ""}replay ${replay}` : "no replay"}`);
|
|
489
520
|
return participant;
|
|
490
521
|
}
|
|
522
|
+
async restartWithPersona(id, patch) {
|
|
523
|
+
const participant = this.participants.get(id);
|
|
524
|
+
if (!participant || participant.kind !== "agent")
|
|
525
|
+
throw new Error("no such agent");
|
|
526
|
+
const runtime = this.runtimes.get(id);
|
|
527
|
+
const online = !!runtime && runtime.agent.alive;
|
|
528
|
+
if (online && runtime.turnActive)
|
|
529
|
+
throw new Error(`${participant.name} is in the middle of a reply; try again when it is idle`);
|
|
530
|
+
if (online) {
|
|
531
|
+
try {
|
|
532
|
+
await this.takeNotes(id);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
this.log.warn(`${participant.name}: notes before the restart failed (${describeError(error)}); it restarts with the notes it had`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
this.updatePersona(id, patch);
|
|
539
|
+
return this.respawnAgent(id, { memory: true, reason: "its role changed" });
|
|
540
|
+
}
|
|
491
541
|
updateNotes(id, notes) {
|
|
492
542
|
const participant = this.participants.get(id);
|
|
493
543
|
if (!participant || participant.kind !== "agent")
|
|
@@ -510,7 +560,15 @@ export class Room extends EventEmitter {
|
|
|
510
560
|
throw new Error(`${participant.name} is in the middle of a reply; try again when it is idle`);
|
|
511
561
|
const header = buildHeader(this.effectiveSettings(), this.personaOf(participant), this.roster(), this.hops, ["hidden turn: notes only, nothing is posted"]);
|
|
512
562
|
runtime.log.info("notes: hidden turn");
|
|
513
|
-
|
|
563
|
+
participant.notesTurn = true;
|
|
564
|
+
this.push({ type: "participant", participant });
|
|
565
|
+
try {
|
|
566
|
+
await this.executeTurn(participant, runtime, [{ type: "text", text: `${header}\n\n${NOTES_ONLY_PROMPT}` }], null, true);
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
participant.notesTurn = undefined;
|
|
570
|
+
this.push({ type: "participant", participant });
|
|
571
|
+
}
|
|
514
572
|
return participant;
|
|
515
573
|
}
|
|
516
574
|
keepNotes(participant, runtime, notes, via) {
|
|
@@ -532,14 +590,14 @@ export class Room extends EventEmitter {
|
|
|
532
590
|
participant.status = "error";
|
|
533
591
|
participant.statusDetail = "its context filled up again right after a respawn; it needs you (respawn it by hand, with fewer replayed messages)";
|
|
534
592
|
this.push({ type: "participant", participant });
|
|
535
|
-
this.postSystem(`${participant.name} ran out of context again (${detail.slice(0, 120)}); it was respawned once already and now needs you.`, "human");
|
|
593
|
+
this.postSystem(`${participant.name} ran out of context again (${detail.slice(0, 120)}); it was respawned once already and now needs you.`, "human", false, { tone: "error" });
|
|
536
594
|
runtime.log.warn(`context full again within 10 minutes: no automatic respawn`);
|
|
537
595
|
return;
|
|
538
596
|
}
|
|
539
597
|
participant.autoRespawnAt = Date.now();
|
|
540
598
|
this.push({ type: "participant", participant });
|
|
541
599
|
const memory = !!participant.notes;
|
|
542
|
-
this.postSystem(`${participant.name} ran out of context (${detail.slice(0, 120)}); it is respawned ${memory ? `with its notes and the last ${this.settings.replayAfterRestart} messages` : `with the last ${this.settings.replayAfterRestart} messages (it had no notes)`}.`, "human");
|
|
600
|
+
this.postSystem(`${participant.name} ran out of context (${detail.slice(0, 120)}); it is respawned ${memory ? `with its notes and the last ${this.settings.replayAfterRestart} messages` : `with the last ${this.settings.replayAfterRestart} messages (it had no notes)`}.`, "human", false, { tone: "error" });
|
|
543
601
|
runtime.log.warn(`context full: ${detail}; respawn with ${memory ? "notes" : "no notes"}`);
|
|
544
602
|
setImmediate(() => {
|
|
545
603
|
void (memory ? this.respawnAgent(participant.id, { memory: true }) : this.reconnectAfterFull(participant.id)).catch((error) => this.notice(`${participant.name}: respawn after a full context failed: ${describeError(error)}`, "error"));
|
|
@@ -595,7 +653,7 @@ export class Room extends EventEmitter {
|
|
|
595
653
|
}
|
|
596
654
|
this.focused = true;
|
|
597
655
|
this.push(this.roomEvent());
|
|
598
|
-
this.postSystem(`Hush: ${stopped ? `${stopped} repl${stopped > 1 ? "ies" : "y"} stopped; ` : ""}everyone waits until ${this.humanName} writes again
|
|
656
|
+
this.postSystem(`Hush: ${stopped ? `${stopped} repl${stopped > 1 ? "ies" : "y"} stopped; ` : ""}everyone waits until ${this.humanName} writes again.`, undefined, false, { tone: "hush" });
|
|
599
657
|
}
|
|
600
658
|
rename(name) {
|
|
601
659
|
const trimmed = name.trim();
|
|
@@ -612,124 +670,20 @@ export class Room extends EventEmitter {
|
|
|
612
670
|
updateSettings(patch) {
|
|
613
671
|
const next = { ...this.settings };
|
|
614
672
|
const changed = [];
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
673
|
+
let unknownRefs = [];
|
|
674
|
+
for (const key of Object.keys(ROOM_SETTINGS_SPEC)) {
|
|
675
|
+
if (patch[key] === undefined || ROOM_SETTINGS_SPEC[key].kind === "own-path")
|
|
676
|
+
continue;
|
|
677
|
+
let value = coerceSetting(key, patch[key]);
|
|
678
|
+
if (key === "customRules") {
|
|
679
|
+
const resolved = this.resolveRuleReferences(value);
|
|
680
|
+
unknownRefs = resolved.unknown;
|
|
681
|
+
value = resolved.stored;
|
|
624
682
|
}
|
|
625
|
-
|
|
626
|
-
setNumber("hopLimit", 0, 10_000);
|
|
627
|
-
setNumber("fullBriefEveryTurns", 1, 10_000);
|
|
628
|
-
setNumber("fullBriefEveryTokens", 1000, 10_000_000);
|
|
629
|
-
setNumber("replayAfterRestart", 0, 200);
|
|
630
|
-
setNumber("backlogCap", 1, 1000);
|
|
631
|
-
if (patch.replyDelay !== undefined) {
|
|
632
|
-
const value = Number(patch.replyDelay);
|
|
633
|
-
if (!Number.isFinite(value) || value < 0 || value > 120)
|
|
634
|
-
throw new Error("replyDelay must be between 0 and 120 seconds");
|
|
635
|
-
next.replyDelay = value;
|
|
636
|
-
}
|
|
637
|
-
const setText = (key, max) => {
|
|
638
|
-
if (patch[key] === undefined)
|
|
639
|
-
return;
|
|
640
|
-
const value = String(patch[key]).slice(0, max);
|
|
641
|
-
if (value !== next[key]) {
|
|
683
|
+
if (JSON.stringify(value) !== JSON.stringify(next[key])) {
|
|
642
684
|
next[key] = value;
|
|
643
685
|
changed.push(key);
|
|
644
686
|
}
|
|
645
|
-
};
|
|
646
|
-
setText("topic", 2000);
|
|
647
|
-
setText("emoji", 8);
|
|
648
|
-
setText("humanDescription", 200);
|
|
649
|
-
let unknownRefs = [];
|
|
650
|
-
if (patch.customRules !== undefined) {
|
|
651
|
-
const resolved = this.resolveRuleReferences(String(patch.customRules).slice(0, 4000));
|
|
652
|
-
unknownRefs = resolved.unknown;
|
|
653
|
-
if (resolved.stored !== next.customRules) {
|
|
654
|
-
next.customRules = resolved.stored;
|
|
655
|
-
changed.push("customRules");
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (patch.humanDescriptionMode !== undefined) {
|
|
659
|
-
const mode = String(patch.humanDescriptionMode);
|
|
660
|
-
if (mode !== "inherit" && mode !== "override" && mode !== "append" && mode !== "none")
|
|
661
|
-
throw new Error("humanDescriptionMode must be inherit, override, append or none");
|
|
662
|
-
if (mode !== next.humanDescriptionMode) {
|
|
663
|
-
next.humanDescriptionMode = mode;
|
|
664
|
-
changed.push("humanDescriptionMode");
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
if (patch.refereeAction !== undefined) {
|
|
668
|
-
const action = String(patch.refereeAction);
|
|
669
|
-
if (action !== "next-header" && action !== "retry-hidden")
|
|
670
|
-
throw new Error("refereeAction must be next-header or retry-hidden");
|
|
671
|
-
if (action !== next.refereeAction) {
|
|
672
|
-
next.refereeAction = action;
|
|
673
|
-
changed.push("refereeAction");
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (patch.turnTaking !== undefined) {
|
|
677
|
-
const mode = String(patch.turnTaking);
|
|
678
|
-
if (mode !== "parallel" && mode !== "one-at-a-time")
|
|
679
|
-
throw new Error("turnTaking must be parallel or one-at-a-time");
|
|
680
|
-
if (mode !== next.turnTaking) {
|
|
681
|
-
next.turnTaking = mode;
|
|
682
|
-
changed.push("turnTaking");
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (patch.agentsWakeEachOther !== undefined) {
|
|
686
|
-
const on = patch.agentsWakeEachOther === true || patch.agentsWakeEachOther === "true";
|
|
687
|
-
if (on !== next.agentsWakeEachOther) {
|
|
688
|
-
next.agentsWakeEachOther = on;
|
|
689
|
-
changed.push("agentsWakeEachOther");
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
if (patch.waitWhileHumanTypes !== undefined) {
|
|
693
|
-
const on = patch.waitWhileHumanTypes === true || patch.waitWhileHumanTypes === "true";
|
|
694
|
-
if (on !== next.waitWhileHumanTypes) {
|
|
695
|
-
next.waitWhileHumanTypes = on;
|
|
696
|
-
changed.push("waitWhileHumanTypes");
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
if (patch.language !== undefined) {
|
|
700
|
-
const raw = String(patch.language).trim();
|
|
701
|
-
const language = !raw || raw === "follow-human" ? { mode: "follow-human" } : { mode: "fixed", language: raw };
|
|
702
|
-
if (JSON.stringify(language) !== JSON.stringify(next.language)) {
|
|
703
|
-
next.language = language;
|
|
704
|
-
changed.push("language");
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (patch.tools !== undefined) {
|
|
708
|
-
const tools = String(patch.tools);
|
|
709
|
-
if (tools !== "on-request" && tools !== "never")
|
|
710
|
-
throw new Error("tools must be on-request or never");
|
|
711
|
-
if (tools !== next.tools) {
|
|
712
|
-
next.tools = tools;
|
|
713
|
-
changed.push("tools");
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (patch.maxSentences !== undefined) {
|
|
717
|
-
const value = patch.maxSentences === null || patch.maxSentences === "" ? null : Number(patch.maxSentences);
|
|
718
|
-
if (value !== null && (!Number.isInteger(value) || value < 1 || value > 100))
|
|
719
|
-
throw new Error("maxSentences must be 1-100 or empty");
|
|
720
|
-
if (value !== next.maxSentences) {
|
|
721
|
-
next.maxSentences = value;
|
|
722
|
-
changed.push("maxSentences");
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
for (const key of ["headerRules", "showVendorInRoster"]) {
|
|
726
|
-
if (patch[key] !== undefined) {
|
|
727
|
-
const value = patch[key] === true || patch[key] === "true";
|
|
728
|
-
if (value !== next[key]) {
|
|
729
|
-
next[key] = value;
|
|
730
|
-
changed.push(key);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
687
|
}
|
|
734
688
|
this.settings = next;
|
|
735
689
|
this.push(this.roomEvent());
|
|
@@ -987,20 +941,25 @@ export class Room extends EventEmitter {
|
|
|
987
941
|
const spec = recipe.build({ model: launch.model });
|
|
988
942
|
const transcript = new Transcript(join(this.dataDir, "transcripts"), name);
|
|
989
943
|
log.info(`spawning ${spec.command} ${spec.args.join(" ")} (cwd ${cwd}); transcript ${transcript.path}`);
|
|
944
|
+
const stderrTail = [];
|
|
990
945
|
let agent;
|
|
991
946
|
try {
|
|
992
947
|
agent = new AcpAgent({ ...spec, cwd }, {
|
|
993
948
|
onSessionUpdate: (_sessionId, update) => this.onSessionUpdate(id, update),
|
|
994
949
|
onPermissionRequest: (params) => this.onPermissionRequest(id, params),
|
|
995
|
-
onStderr: (line) =>
|
|
950
|
+
onStderr: (line) => {
|
|
951
|
+
log.info(`stderr: ${line}`);
|
|
952
|
+
stderrTail.push(line);
|
|
953
|
+
if (stderrTail.length > 10)
|
|
954
|
+
stderrTail.shift();
|
|
955
|
+
},
|
|
996
956
|
onExit: (code, signal) => this.onAgentExit(id, code, signal, agent),
|
|
997
957
|
onRaw: (direction, message) => transcript.record(direction, message),
|
|
998
958
|
onProtocolError: (text) => log.warn(`protocol: ${text}`),
|
|
999
959
|
});
|
|
1000
960
|
}
|
|
1001
961
|
catch (error) {
|
|
1002
|
-
this.failStart(participant, error, fresh);
|
|
1003
|
-
throw error;
|
|
962
|
+
throw this.failStart(participant, error, fresh, stderrTail);
|
|
1004
963
|
}
|
|
1005
964
|
try {
|
|
1006
965
|
const init = await agent.initialize({ name: "viberoom", version: "0.2.0" });
|
|
@@ -1093,6 +1052,7 @@ export class Room extends EventEmitter {
|
|
|
1093
1052
|
this.notice(`${name}: ${w}`, "warn");
|
|
1094
1053
|
participant.status = "idle";
|
|
1095
1054
|
participant.statusDetail = undefined;
|
|
1055
|
+
participant.trouble = undefined;
|
|
1096
1056
|
this.push({ type: "participant", participant });
|
|
1097
1057
|
if (fresh) {
|
|
1098
1058
|
const detail = this.settings.showVendorInRoster ? `${recipe.label}${participant.model ? `, model ${participant.model}` : ""}` : "agent";
|
|
@@ -1124,8 +1084,7 @@ export class Room extends EventEmitter {
|
|
|
1124
1084
|
catch (error) {
|
|
1125
1085
|
agent.kill();
|
|
1126
1086
|
this.forgetRuntime(id);
|
|
1127
|
-
this.failStart(participant, error, fresh);
|
|
1128
|
-
throw error;
|
|
1087
|
+
throw this.failStart(participant, error, fresh, stderrTail);
|
|
1129
1088
|
}
|
|
1130
1089
|
}
|
|
1131
1090
|
async removeParticipant(id) {
|
|
@@ -1169,11 +1128,20 @@ export class Room extends EventEmitter {
|
|
|
1169
1128
|
const participant = this.participants.get(id);
|
|
1170
1129
|
if (!runtime || !participant)
|
|
1171
1130
|
throw new Error("no such agent");
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1131
|
+
if (runtime.turnActive) {
|
|
1132
|
+
runtime.agent.cancel(runtime.sessionId);
|
|
1133
|
+
this.cancelPermissionsOf(id);
|
|
1134
|
+
this.notice(`${participant.name}: stop requested.`, "info");
|
|
1135
|
+
return "turn";
|
|
1136
|
+
}
|
|
1137
|
+
if (runtime.pendingTurn || runtime.delayTimer || this.floorQueue.includes(id)) {
|
|
1138
|
+
this.dropScheduledTurn(id);
|
|
1139
|
+
this.notice(`${participant.name}: stopped before it began; the turn is dropped.`, "info");
|
|
1140
|
+
this.postSystem(`${participant.name} was stopped by ${this.humanName} before it began.`, undefined, false, { tone: "attention" });
|
|
1141
|
+
return "queued";
|
|
1142
|
+
}
|
|
1143
|
+
this.notice(`${participant.name}: nothing to stop, it is not writing.`, "info");
|
|
1144
|
+
return "nothing";
|
|
1177
1145
|
}
|
|
1178
1146
|
async setConfig(id, configId, value) {
|
|
1179
1147
|
const runtime = this.runtimes.get(id);
|
|
@@ -1260,7 +1228,7 @@ export class Room extends EventEmitter {
|
|
|
1260
1228
|
if (wanted.length) {
|
|
1261
1229
|
if (this.hops >= this.hopLimit) {
|
|
1262
1230
|
const who = agentTargets.length ? message.toNames.join(", ") : "the other vibemates";
|
|
1263
|
-
this.postSystem(`Hop limit ${this.hopLimit} reached: ${who} will not be prompted until ${this.humanName} writes again
|
|
1231
|
+
this.postSystem(`Hop limit ${this.hopLimit} reached: ${who} will not be prompted until ${this.humanName} writes again.`, undefined, false, { tone: "attention" });
|
|
1264
1232
|
}
|
|
1265
1233
|
else {
|
|
1266
1234
|
this.hops += 1;
|
|
@@ -1294,6 +1262,256 @@ export class Room extends EventEmitter {
|
|
|
1294
1262
|
return undefined;
|
|
1295
1263
|
return { items, channel, canCreate: channel === "tool" };
|
|
1296
1264
|
}
|
|
1265
|
+
skillsForDesign(participantId) {
|
|
1266
|
+
if (!this.skills)
|
|
1267
|
+
return undefined;
|
|
1268
|
+
const runtime = this.runtimes.get(participantId);
|
|
1269
|
+
const channel = runtime?.skillChannel === "tool" ? "tool" : "marker";
|
|
1270
|
+
return {
|
|
1271
|
+
library: this.skills.library.list().filter((s) => !s.problems.length && !s.draft && s.agentInvocable).map((s) => ({ name: s.name, description: s.description })),
|
|
1272
|
+
channel,
|
|
1273
|
+
canCreate: channel === "tool",
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
agentInRoom(participantId) {
|
|
1277
|
+
const participant = this.participants.get(participantId);
|
|
1278
|
+
if (!participant || !this.runtimes.has(participantId))
|
|
1279
|
+
throw new Error("this agent is not in the room any more");
|
|
1280
|
+
return participant;
|
|
1281
|
+
}
|
|
1282
|
+
describeRoomForAgent(participantId) {
|
|
1283
|
+
const participant = this.agentInRoom(participantId);
|
|
1284
|
+
const shape = this.templateOf();
|
|
1285
|
+
const skills = this.skills ? this.skills.library.list().filter((s) => !s.problems.length && !s.draft).map((s) => ({ name: s.name, description: s.description })) : [];
|
|
1286
|
+
const templates = this.skills ? this.skills.templates.list().map((t) => ({ id: t.id, name: t.name, builtin: !!t.builtin })) : [];
|
|
1287
|
+
return {
|
|
1288
|
+
room: { name: this.settings.name, topic: this.settings.topic, emoji: this.settings.emoji, dir: this.dir },
|
|
1289
|
+
human: this.settings.humanName,
|
|
1290
|
+
you: participant.name,
|
|
1291
|
+
settings: describeSettings({ ...this.settings, customRules: this.renderRuleReferences(this.settings.customRules) }),
|
|
1292
|
+
rules: ruleLines(this.renderRuleReferences(this.settings.customRules)),
|
|
1293
|
+
vibemates: shape.vibemates.map((v) => {
|
|
1294
|
+
const role = v.role ?? "";
|
|
1295
|
+
const own = v.name === participant.name;
|
|
1296
|
+
return {
|
|
1297
|
+
name: v.name,
|
|
1298
|
+
tagline: v.tagline ?? "",
|
|
1299
|
+
...(own ? { role } : { rolePrivate: true, roleLength: role.length }),
|
|
1300
|
+
avatar: v.avatar ?? "",
|
|
1301
|
+
skills: v.skills ?? [],
|
|
1302
|
+
agentType: v.agentType,
|
|
1303
|
+
};
|
|
1304
|
+
}),
|
|
1305
|
+
skills,
|
|
1306
|
+
templates,
|
|
1307
|
+
yourBrief: buildBrief(this.settings, this.personaOf(participant), this.roster(), undefined, this.skillsForPrompt(participant, this.runtimes.get(participant.id))),
|
|
1308
|
+
howTo: "Settings are proposed by key with the values above; rules are one per line in customRules; a vibemate is { name, tagline, role, avatar, skills }. Another vibemate's role is private: you learn only that it has one and how long it is, and you may still propose a new one, which the human reads in full on the card. Check a design with lint_room_design, then create_template (a file for the human to pick) or propose_room_changes (a card the human applies).",
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
lintDesignForAgent(participantId, kind, design) {
|
|
1312
|
+
this.agentInRoom(participantId);
|
|
1313
|
+
const result = lintRoomDesign(design, { ...this.designContext(kind), skills: this.skillsForDesign(participantId) });
|
|
1314
|
+
return { ok: !result.errors.length, errors: result.errors.map((e) => e.message), warnings: result.warnings.map((w) => w.message), preview: result.preview };
|
|
1315
|
+
}
|
|
1316
|
+
readMessageForAgent(participantId, seq, around) {
|
|
1317
|
+
const participant = this.agentInRoom(participantId);
|
|
1318
|
+
const window = agentReadableWindow(this.messages, seq, around);
|
|
1319
|
+
if (!window)
|
|
1320
|
+
throw new Error(`no message #${seq} in this room (or it is one the vibemates do not see)`);
|
|
1321
|
+
const view = (m) => ({
|
|
1322
|
+
seq: m.seq,
|
|
1323
|
+
from: m.fromName,
|
|
1324
|
+
to: m.toNames,
|
|
1325
|
+
at: new Date(m.ts).toISOString(),
|
|
1326
|
+
text: m.text,
|
|
1327
|
+
...(m.kind === "system" ? { kind: "system" } : {}),
|
|
1328
|
+
...(m.edited ? { edited: true } : {}),
|
|
1329
|
+
...(m.images && m.images.length ? { images: m.images.map((a, i) => ({ ref: `#${m.seq}.${a.n ?? i + 1}`, name: a.name, path: this.imagePath(a) })) } : {}),
|
|
1330
|
+
...(m.quotes && m.quotes.length ? { quotes: m.quotes.map((q) => ({ n: q.n, seq: q.seq, from: q.fromName, text: q.text })) } : {}),
|
|
1331
|
+
});
|
|
1332
|
+
this.log.info(`read_message: ${participant.name} read #${seq}${around ? ` (around ${around})` : ""}`);
|
|
1333
|
+
return { message: view(window.message), before: window.before.map(view), after: window.after.map(view) };
|
|
1334
|
+
}
|
|
1335
|
+
designContext(kind) {
|
|
1336
|
+
return {
|
|
1337
|
+
kind,
|
|
1338
|
+
humanName: this.settings.humanName,
|
|
1339
|
+
roomName: this.settings.name,
|
|
1340
|
+
base: kind === "room" ? { ...this.settings, customRules: this.renderRuleReferences(this.settings.customRules) } : undefined,
|
|
1341
|
+
knownSkills: this.skills ? this.skills.library.list().map((s) => s.name) : undefined,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
createTemplateForAgent(participantId, design, replace) {
|
|
1345
|
+
const participant = this.agentInRoom(participantId);
|
|
1346
|
+
if (!this.skills)
|
|
1347
|
+
throw new Error("templates are not available in this hub");
|
|
1348
|
+
const result = lintRoomDesign(design, this.designContext("template"));
|
|
1349
|
+
if (result.errors.length)
|
|
1350
|
+
throw new Error(`not saved: ${result.errors.map((e) => e.message).join("; ")}`);
|
|
1351
|
+
const settings = result.settings;
|
|
1352
|
+
const rest = {};
|
|
1353
|
+
for (const key of Object.keys(design.settings ?? {}))
|
|
1354
|
+
if (AGENT_SETTINGS.includes(key))
|
|
1355
|
+
rest[key] = settings[key];
|
|
1356
|
+
const draft = {
|
|
1357
|
+
name: String(design.name).trim(),
|
|
1358
|
+
description: String(design.description ?? "").trim(),
|
|
1359
|
+
emoji: settings.emoji || undefined,
|
|
1360
|
+
settings: rest,
|
|
1361
|
+
vibemates: (design.vibemates ?? []).map((v) => {
|
|
1362
|
+
const out = { name: v.name.trim() };
|
|
1363
|
+
if (v.tagline?.trim())
|
|
1364
|
+
out.tagline = v.tagline.trim();
|
|
1365
|
+
if (v.role?.trim())
|
|
1366
|
+
out.role = v.role.trim();
|
|
1367
|
+
if (v.avatar?.trim())
|
|
1368
|
+
out.avatar = v.avatar.trim();
|
|
1369
|
+
if (v.skills?.length)
|
|
1370
|
+
out.skills = v.skills.map((s) => s.trim()).filter(Boolean);
|
|
1371
|
+
if (typeof v.replyDelay === "number")
|
|
1372
|
+
out.replyDelay = v.replyDelay;
|
|
1373
|
+
return out;
|
|
1374
|
+
}),
|
|
1375
|
+
};
|
|
1376
|
+
const library = this.skills.templates;
|
|
1377
|
+
const wanted = templateId(draft.name);
|
|
1378
|
+
const existing = library.list().find((t) => t.id === wanted);
|
|
1379
|
+
let saved;
|
|
1380
|
+
if (existing && replace) {
|
|
1381
|
+
if (existing.builtin)
|
|
1382
|
+
throw new Error(`"${existing.name}" is a template viberoom ships and cannot be replaced; pick another name`);
|
|
1383
|
+
saved = library.overwrite(wanted, draft);
|
|
1384
|
+
}
|
|
1385
|
+
else
|
|
1386
|
+
saved = library.save(draft);
|
|
1387
|
+
this.skills.templatesChanged();
|
|
1388
|
+
const warnings = result.warnings.map((w) => w.message);
|
|
1389
|
+
this.postSystem(`${participant.name} ${existing && replace ? "updated" : "created"} the room template "${saved.name}" (${saved.vibemates.map((v) => v.name).join(", ") || "no vibemates"}); it is in the picker under New room.`);
|
|
1390
|
+
this.log.info(`templates: ${participant.name} ${existing && replace ? "updated" : "created"} "${saved.name}" (${saved.id})`);
|
|
1391
|
+
return {
|
|
1392
|
+
ok: true,
|
|
1393
|
+
message: `Template "${saved.name}" saved as ${saved.id}${existing && !replace ? ` (the name was taken, so the id got a number; pass replace: true to update your own template instead)` : ""}. The human creates a room from it under New room; nothing in this room changed.${warnings.length ? ` Warnings: ${warnings.join("; ")}` : ""}`,
|
|
1394
|
+
id: saved.id,
|
|
1395
|
+
path: join(library.dir, saved.id, "template.json"),
|
|
1396
|
+
warnings,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
proposeRoomChanges(participantId, why, changes) {
|
|
1400
|
+
const participant = this.agentInRoom(participantId);
|
|
1401
|
+
const shape = this.templateOf();
|
|
1402
|
+
const current = shape.vibemates;
|
|
1403
|
+
const vibes = applyVibemateChanges(current, changes.vibemates);
|
|
1404
|
+
if (vibes.errors.length)
|
|
1405
|
+
throw new Error(`not proposed: ${vibes.errors.join("; ")}`);
|
|
1406
|
+
const touched = vibes.ops.flatMap((op) => [op.name, ...(op.fields ?? []).filter((f) => f.field === "name").map((f) => f.to)]);
|
|
1407
|
+
const result = lintRoomDesign({ settings: changes.settings, vibemates: vibes.next }, { ...this.designContext("room"), changedVibemates: touched });
|
|
1408
|
+
if (result.errors.length)
|
|
1409
|
+
throw new Error(`not proposed: ${result.errors.map((e) => e.message).join("; ")}`);
|
|
1410
|
+
const base = this.designContext("room").base;
|
|
1411
|
+
const settingChanges = diffSettings(base, result.settings);
|
|
1412
|
+
if (!settingChanges.length && !vibes.ops.length)
|
|
1413
|
+
throw new Error("not proposed: the change set leaves the room as it is");
|
|
1414
|
+
const touchesOwn = vibes.ops.some((op) => op.name.toLowerCase() === participant.name.toLowerCase()) || settingChanges.some((c) => c.key === "customRules");
|
|
1415
|
+
const proposal = {
|
|
1416
|
+
key: randomUUID(),
|
|
1417
|
+
participantId,
|
|
1418
|
+
participantName: participant.name,
|
|
1419
|
+
ts: Date.now(),
|
|
1420
|
+
why: String(why ?? "").trim().slice(0, 600),
|
|
1421
|
+
settings: settingChanges,
|
|
1422
|
+
vibemates: vibes.ops,
|
|
1423
|
+
warnings: result.warnings.map((w) => w.message),
|
|
1424
|
+
touchesOwn,
|
|
1425
|
+
status: "pending",
|
|
1426
|
+
};
|
|
1427
|
+
const ids = {};
|
|
1428
|
+
for (const op of vibes.ops) {
|
|
1429
|
+
const target = op.op === "add" ? undefined : this.findByName(op.name);
|
|
1430
|
+
if (target)
|
|
1431
|
+
ids[op.name] = target.id;
|
|
1432
|
+
}
|
|
1433
|
+
this.proposalPlans.set(proposal.key, { settings: settingChanges, vibemates: vibes.next, ops: vibes.ops, ids });
|
|
1434
|
+
this.proposals.set(proposal.key, proposal);
|
|
1435
|
+
this.push({ type: "proposal", proposal });
|
|
1436
|
+
const what = [...settingChanges.map((c) => c.key), ...vibes.ops.map((o) => `${o.op} ${o.name}`)].join(", ");
|
|
1437
|
+
this.postSystem(`${participant.name} proposes changes to the room (${what}); apply or reject them on the card.`, "human", false, { tone: "attention" });
|
|
1438
|
+
this.log.info(`proposal ${proposal.key} from ${participant.name}: ${what}`);
|
|
1439
|
+
return {
|
|
1440
|
+
ok: true,
|
|
1441
|
+
message: `Proposal sent to ${this.settings.humanName} as a card in the room (${what}). Nothing changes until they apply it; you will see a room line with the outcome.${proposal.warnings.length ? ` Warnings shown on the card: ${proposal.warnings.join("; ")}` : ""}`,
|
|
1442
|
+
key: proposal.key,
|
|
1443
|
+
warnings: proposal.warnings,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
async resolveProposal(key, accept) {
|
|
1447
|
+
const proposal = this.proposals.get(key);
|
|
1448
|
+
const plan = this.proposalPlans.get(key);
|
|
1449
|
+
if (!proposal || !plan)
|
|
1450
|
+
throw new Error("no such pending proposal");
|
|
1451
|
+
if (proposal.status !== "pending")
|
|
1452
|
+
return proposal;
|
|
1453
|
+
const what = [...proposal.settings.map((c) => c.key), ...proposal.vibemates.map((o) => `${o.op} ${o.name}`)].join(", ");
|
|
1454
|
+
if (!accept) {
|
|
1455
|
+
proposal.status = "rejected";
|
|
1456
|
+
this.proposalPlans.delete(key);
|
|
1457
|
+
this.push({ type: "proposal.resolved", key, status: "rejected" });
|
|
1458
|
+
this.postSystem(`${this.settings.humanName} rejected ${proposal.participantName}'s proposal (${what}).`);
|
|
1459
|
+
return proposal;
|
|
1460
|
+
}
|
|
1461
|
+
const skipped = [];
|
|
1462
|
+
for (const op of plan.ops) {
|
|
1463
|
+
const known = plan.ids[op.name];
|
|
1464
|
+
const existing = known ? this.participants.get(known) : this.findByName(op.name);
|
|
1465
|
+
if (op.op !== "add" && (!existing || existing.kind !== "agent" || existing.status === "left")) {
|
|
1466
|
+
skipped.push(`${op.op} ${op.name} (no longer in the room)`);
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (op.op === "remove") {
|
|
1470
|
+
if (existing && existing.kind === "agent")
|
|
1471
|
+
await this.removeParticipant(existing.id);
|
|
1472
|
+
}
|
|
1473
|
+
else if (op.op === "update") {
|
|
1474
|
+
if (!existing || existing.kind !== "agent")
|
|
1475
|
+
continue;
|
|
1476
|
+
const target = plan.vibemates.find((v) => v.name === op.name) ?? plan.vibemates.find((v) => op.fields?.some((f) => f.field === "name" && f.to === v.name));
|
|
1477
|
+
const patch = {};
|
|
1478
|
+
for (const f of op.fields ?? []) {
|
|
1479
|
+
if (f.field === "name")
|
|
1480
|
+
patch.name = f.to;
|
|
1481
|
+
else if (f.field === "tagline")
|
|
1482
|
+
patch.tagline = target?.tagline ?? f.to;
|
|
1483
|
+
else if (f.field === "role")
|
|
1484
|
+
patch.role = target?.role ?? f.to;
|
|
1485
|
+
else if (f.field === "avatar")
|
|
1486
|
+
patch.avatar = target?.avatar ?? f.to;
|
|
1487
|
+
else if (f.field === "skills")
|
|
1488
|
+
patch.skills = target?.skills ?? [];
|
|
1489
|
+
else if (f.field === "replyDelay")
|
|
1490
|
+
patch.replyDelay = target?.replyDelay ?? null;
|
|
1491
|
+
}
|
|
1492
|
+
this.updatePersona(existing.id, patch);
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
const v = plan.vibemates.find((x) => x.name === op.name);
|
|
1496
|
+
if (!v || this.findByName(op.name))
|
|
1497
|
+
skipped.push(`add ${op.name} (the name is taken now)`);
|
|
1498
|
+
else
|
|
1499
|
+
this.addUnstaffed({ name: v.name, tagline: v.tagline, role: v.role, avatar: v.avatar, skills: v.skills });
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
if (plan.settings.length) {
|
|
1503
|
+
const patch = {};
|
|
1504
|
+
for (const c of plan.settings)
|
|
1505
|
+
patch[c.key] = c.key === "language" ? c.to : c.to;
|
|
1506
|
+
this.updateSettings(patch);
|
|
1507
|
+
}
|
|
1508
|
+
proposal.status = "applied";
|
|
1509
|
+
proposal.skipped = skipped;
|
|
1510
|
+
this.proposalPlans.delete(key);
|
|
1511
|
+
this.push({ type: "proposal.resolved", key, status: "applied", skipped });
|
|
1512
|
+
this.postSystem(`${this.settings.humanName} applied ${proposal.participantName}'s proposal (${what}).${skipped.length ? ` Not applied: ${skipped.join("; ")}.` : ""}`);
|
|
1513
|
+
return proposal;
|
|
1514
|
+
}
|
|
1297
1515
|
createSkillForAgent(participantId, input) {
|
|
1298
1516
|
const participant = this.participants.get(participantId);
|
|
1299
1517
|
if (!participant || !this.runtimes.has(participantId))
|
|
@@ -1708,6 +1926,7 @@ export class Room extends EventEmitter {
|
|
|
1708
1926
|
const attached = seesImages && (m.to.length === 0 || m.to.includes(id));
|
|
1709
1927
|
return m.images.map((a, i) => ({ n: a.n ?? i + 1, ref: `#${m.seq}.${a.n ?? i + 1}`, name: a.name, path: this.imagePath(a), mimeType: a.mimeType, attached, forNames: m.to.length ? m.toNames : [] }));
|
|
1710
1928
|
};
|
|
1929
|
+
const backlogQuotes = (m) => m.quotes && m.quotes.length ? m.quotes.map((q) => ({ n: q.n, seq: q.seq, fromName: q.fromName, ts: q.ts, text: q.text })) : undefined;
|
|
1711
1930
|
const prompt = composePrompt({
|
|
1712
1931
|
brief: briefReason ? buildBrief(settings, persona, roster, runtime.notesForBrief ?? (participant.notes && (participant.notesSeq ?? -1) >= runtime.lastBriefSeq ? participant.notes : undefined), skillsForPrompt) : undefined,
|
|
1713
1932
|
header: buildHeader(settings, persona, roster, this.hops, notes, skillsForPrompt),
|
|
@@ -1720,6 +1939,7 @@ export class Room extends EventEmitter {
|
|
|
1720
1939
|
toNames: m.toNames,
|
|
1721
1940
|
text: m.text,
|
|
1722
1941
|
images: backlogImages(m),
|
|
1942
|
+
quotes: backlogQuotes(m),
|
|
1723
1943
|
}),
|
|
1724
1944
|
omitted,
|
|
1725
1945
|
personaName: participant.name,
|
|
@@ -1819,7 +2039,7 @@ export class Room extends EventEmitter {
|
|
|
1819
2039
|
if (isContextFullError(failure))
|
|
1820
2040
|
this.contextFull(participant, runtime, failure ?? "");
|
|
1821
2041
|
else
|
|
1822
|
-
this.postSystem(`${participant.name} could not answer: ${(failure ?? "no result").slice(0, 240)}
|
|
2042
|
+
this.postSystem(`${participant.name} could not answer: ${(failure ?? "no result").slice(0, 240)}`, undefined, false, { tone: "error" });
|
|
1823
2043
|
return null;
|
|
1824
2044
|
}
|
|
1825
2045
|
return this.finalizeTurn(participant, runtime, draft, result, Date.now() - startedAt, retry, published, publishedAt);
|
|
@@ -1881,10 +2101,13 @@ export class Room extends EventEmitter {
|
|
|
1881
2101
|
if (retry) {
|
|
1882
2102
|
this.closeRetry(retry, cancelled ? "the correction turn was stopped; nothing was posted" : "the agent withdrew the reply");
|
|
1883
2103
|
if (cancelled)
|
|
1884
|
-
this.postSystem(`${participant.name} was stopped
|
|
2104
|
+
this.postSystem(`${participant.name} was stopped by ${this.humanName}.`, undefined, false, { tone: "attention" });
|
|
2105
|
+
}
|
|
2106
|
+
else if (cancelled) {
|
|
2107
|
+
this.postSystem(`${participant.name} was stopped by ${this.humanName}.`, undefined, false, { tone: "attention" });
|
|
1885
2108
|
}
|
|
1886
2109
|
else {
|
|
1887
|
-
this.postSystem(
|
|
2110
|
+
this.postSystem(`${participant.name} read the room and has nothing to add.`);
|
|
1888
2111
|
}
|
|
1889
2112
|
return null;
|
|
1890
2113
|
}
|
|
@@ -1894,7 +2117,7 @@ export class Room extends EventEmitter {
|
|
|
1894
2117
|
participant.failedTurns = (participant.failedTurns ?? 0) + 1;
|
|
1895
2118
|
participant.statusDetail = `agent error: ${text.replace(/\s+/g, " ").slice(0, 120)}${text.length > 120 ? "…" : ""}`;
|
|
1896
2119
|
this.push({ type: "participant", participant });
|
|
1897
|
-
this.postSystem(`${participant.name}'s agent reported an error instead of a reply: ${text.slice(0, 200)}${text.length > 200 ? "…" : ""}
|
|
2120
|
+
this.postSystem(`${participant.name}'s agent reported an error instead of a reply: ${text.slice(0, 200)}${text.length > 200 ? "…" : ""}`, undefined, false, { tone: "error" });
|
|
1898
2121
|
runtime.log.warn(`adapter error text treated as failed turn: ${text.slice(0, 200)}`);
|
|
1899
2122
|
if (retry)
|
|
1900
2123
|
this.closeRetry(retry, "the agent reported an error instead of a corrected reply");
|
|
@@ -1945,6 +2168,7 @@ export class Room extends EventEmitter {
|
|
|
1945
2168
|
text,
|
|
1946
2169
|
streaming: false,
|
|
1947
2170
|
stopReason: result.stopReason,
|
|
2171
|
+
...(cancelled ? { stoppedBy: this.humanName } : {}),
|
|
1948
2172
|
usage: result.usage ?? null,
|
|
1949
2173
|
durationMs,
|
|
1950
2174
|
};
|
|
@@ -1952,17 +2176,17 @@ export class Room extends EventEmitter {
|
|
|
1952
2176
|
if (publishedAt !== null && this.messages.some((x) => x.kind === "chat" && x.id !== message.id && x.ts > publishedAt)) {
|
|
1953
2177
|
const at = new Date(draft.ts);
|
|
1954
2178
|
const hhmm = `${String(at.getHours()).padStart(2, "0")}:${String(at.getMinutes()).padStart(2, "0")}`;
|
|
1955
|
-
this.postSystem(`${participant.name} finished the reply started at ${hhmm} · ${
|
|
2179
|
+
this.postSystem(`${participant.name} finished the reply started at ${hhmm} · ${formatDuration(durationMs)}`, "human", false, { refId: message.id, agentId: participant.id });
|
|
1956
2180
|
}
|
|
1957
2181
|
if (retry) {
|
|
1958
2182
|
this.closeRetry(retry, corrections.length ? `corrected reply posted, but it still breaks: ${corrections.map((c) => c.replace(/^reminder:\s*/i, "")).join("; ")}` : "corrected reply posted");
|
|
1959
2183
|
}
|
|
1960
2184
|
if (cancelled) {
|
|
1961
|
-
this.postSystem(`${participant.name} was stopped mid-reply;
|
|
2185
|
+
this.postSystem(`${participant.name} was stopped mid-reply by ${this.humanName}; what it had written stays in the room.`, "agents", false, { tone: "attention" });
|
|
1962
2186
|
return null;
|
|
1963
2187
|
}
|
|
1964
2188
|
if (result.stopReason !== "end_turn") {
|
|
1965
|
-
this.postSystem(`${participant.name} stopped with ${result.stopReason}
|
|
2189
|
+
this.postSystem(`${participant.name} stopped with ${result.stopReason}.`, undefined, false, { tone: "attention" });
|
|
1966
2190
|
}
|
|
1967
2191
|
this.route(message);
|
|
1968
2192
|
return null;
|
|
@@ -2127,21 +2351,25 @@ export class Room extends EventEmitter {
|
|
|
2127
2351
|
}
|
|
2128
2352
|
case "usage_update": {
|
|
2129
2353
|
const u = update;
|
|
2354
|
+
if (emptyUsageReport(runtime.lastUsed, u.used)) {
|
|
2355
|
+
runtime.log.info(`usage report of 0 after ${runtime.lastUsed} tokens ignored (failed request?)`);
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2130
2358
|
participant.contextUsed = u.used;
|
|
2131
2359
|
participant.contextSize = u.size;
|
|
2132
2360
|
if (u.cost)
|
|
2133
2361
|
participant.cost = { amount: u.cost.amount, currency: u.cost.currency };
|
|
2134
|
-
if (runtime.lastUsed
|
|
2362
|
+
if (looksCompacted(runtime.lastUsed, u.used) && !runtime.briefPending) {
|
|
2135
2363
|
runtime.briefPending = `context shrank from ${runtime.lastUsed} to ${u.used} tokens (compaction?)`;
|
|
2136
2364
|
runtime.log.info(`usage dropped ${runtime.lastUsed} -> ${u.used}; brief scheduled`);
|
|
2137
2365
|
participant.contextEvent = { kind: "compacted", at: Date.now(), used: u.used, size: u.size };
|
|
2138
2366
|
runtime.notesDue = overThreshold(u.used, u.size);
|
|
2139
|
-
this.postSystem(`${participant.name} compacted its context (${Math.round(runtime.lastUsed / 1000)}k → ${Math.round(u.used / 1000)}k tokens); the room rules are re-sent with its next turn${participant.notes ? ", with its notes" : ""}.`, "human");
|
|
2367
|
+
this.postSystem(`${participant.name} compacted its context (${Math.round(runtime.lastUsed / 1000)}k → ${Math.round(u.used / 1000)}k tokens); the room rules are re-sent with its next turn${participant.notes ? ", with its notes" : ""}.`, "human", false, { tone: "attention" });
|
|
2140
2368
|
}
|
|
2141
2369
|
if (crossedThreshold(runtime.lastUsed, u.used, u.size)) {
|
|
2142
2370
|
runtime.notesDue = true;
|
|
2143
2371
|
participant.contextEvent = { kind: "threshold", at: Date.now(), used: u.used, size: u.size };
|
|
2144
|
-
this.postSystem(`${participant.name} is at ${Math.round((100 * u.used) / u.size)}% of its context; it will leave notes with its next reply. You can respawn it with memory from its panel.`, "human");
|
|
2372
|
+
this.postSystem(`${participant.name} is at ${Math.round((100 * u.used) / u.size)}% of its context; it will leave notes with its next reply. You can respawn it with memory from its panel.`, "human", false, { tone: "attention" });
|
|
2145
2373
|
runtime.log.info(`context at ${u.used}/${u.size}: notes due`);
|
|
2146
2374
|
}
|
|
2147
2375
|
runtime.lastUsed = u.used;
|
|
@@ -2308,8 +2536,17 @@ export class Room extends EventEmitter {
|
|
|
2308
2536
|
participant.effort = pick("thought_level") ?? participant.effort;
|
|
2309
2537
|
participant.mode = pick("mode") ?? participant.mode;
|
|
2310
2538
|
}
|
|
2311
|
-
failStart(participant, error, fresh) {
|
|
2539
|
+
failStart(participant, error, fresh, stderr = []) {
|
|
2312
2540
|
participant.statusDetail = error instanceof Error ? error.message : String(error);
|
|
2541
|
+
const recipe = getRecipe(participant.agentType ?? "");
|
|
2542
|
+
participant.trouble = classifyStartFailure({
|
|
2543
|
+
error: participant.statusDetail,
|
|
2544
|
+
stderr,
|
|
2545
|
+
vendor: recipe?.vendor ?? participant.agentVendor ?? "The coding agent",
|
|
2546
|
+
loginCommand: recipe?.loginCommand || undefined,
|
|
2547
|
+
installHint: recipe?.installHint || undefined,
|
|
2548
|
+
loginState: recipe?.loginState,
|
|
2549
|
+
});
|
|
2313
2550
|
this.notice(`${participant.name}: failed to start: ${participant.statusDetail}`, "error");
|
|
2314
2551
|
if (fresh) {
|
|
2315
2552
|
participant.status = "error";
|
|
@@ -2321,6 +2558,7 @@ export class Room extends EventEmitter {
|
|
|
2321
2558
|
participant.status = "offline";
|
|
2322
2559
|
this.push({ type: "participant", participant });
|
|
2323
2560
|
}
|
|
2561
|
+
return new Error(`${participant.trouble.what} ${participant.trouble.advice} (${participant.statusDetail})`);
|
|
2324
2562
|
}
|
|
2325
2563
|
cancelPermissionsOf(id) {
|
|
2326
2564
|
for (const [key, entry] of this.permissions) {
|