@qearlyao/familiar 0.2.1 → 0.2.3

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/dist/web.js CHANGED
@@ -12,6 +12,7 @@ import { publicAttachmentPath } from "./generated-media.js";
12
12
  import { materializeInboundAttachments } from "./inbound-attachments.js";
13
13
  import { PROVIDER_DEFAULTS, parseModelRef, supportedThinkingLevels } from "./models.js";
14
14
  import { loadPersona, parsePersonaName } from "./persona.js";
15
+ import { consumeSilentDelta, createSilentFilterState, finalizeSilentFilter, parseAgentReply } from "./silent-marker.js";
15
16
  import { createAuth, sessionCookie, verifyTotp } from "./web-auth.js";
16
17
  import { acceptWebSocket, decodeFrames, encodeFrame, replayEvents } from "./web-events.js";
17
18
  import { isObject, readJsonBody, sendJson, sendText } from "./web-http.js";
@@ -304,6 +305,7 @@ function webMessageFromRecord(config, record, assistantName) {
304
305
  attachments: webAttachments(config, record.attachments),
305
306
  thinking: record.thinking,
306
307
  thinkingMs: record.thinkingMs,
308
+ silent: record.silent || undefined,
307
309
  ts: toUnixMs(record.ts),
308
310
  };
309
311
  }
@@ -364,6 +366,8 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
364
366
  const eventsByChannel = new Map();
365
367
  const runtimeSubscriptions = new Map();
366
368
  const locallyStreamedOutboundIds = new Set();
369
+ const silentFilters = new Map();
370
+ const pendingMessageStarts = new Map();
367
371
  const publish = (event) => {
368
372
  const fullEvent = { ...event, eventId: eventId(), ts: event.ts ?? Date.now() };
369
373
  const events = eventsByChannel.get(fullEvent.channelKey ?? "") ?? [];
@@ -390,30 +394,74 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
390
394
  const publishStoredAgentEvent = (channelKey, messageIdValue, storedEvent, ts) => {
391
395
  if (storedEvent.type === "message_start" && storedEvent.role === "assistant") {
392
396
  locallyStreamedOutboundIds.add(messageIdValue);
397
+ silentFilters.set(messageIdValue, createSilentFilterState());
398
+ pendingMessageStarts.set(messageIdValue, ts);
399
+ }
400
+ const startedSilentMessage = () => {
401
+ if (!pendingMessageStarts.has(messageIdValue))
402
+ return false;
403
+ const startTs = pendingMessageStarts.get(messageIdValue);
404
+ pendingMessageStarts.delete(messageIdValue);
393
405
  publish({
394
406
  type: "message_started",
395
407
  channelKey,
396
408
  messageId: messageIdValue,
397
409
  role: "assistant",
398
410
  who: personaName,
399
- ts,
411
+ ts: startTs,
400
412
  });
401
- }
413
+ return true;
414
+ };
402
415
  if (storedEvent.type === "message_update") {
403
416
  const assistantEvent = storedEvent.assistantMessageEvent;
404
417
  if (assistantEvent.type === "thinking_delta") {
418
+ startedSilentMessage();
405
419
  publishDelta(channelKey, messageIdValue, "thinking", assistantEvent.delta, ts);
406
420
  }
407
421
  if (assistantEvent.type === "text_delta") {
408
- publishDelta(channelKey, messageIdValue, "text", assistantEvent.delta, ts);
422
+ const filter = silentFilters.get(messageIdValue);
423
+ if (!filter) {
424
+ startedSilentMessage();
425
+ publishDelta(channelKey, messageIdValue, "text", assistantEvent.delta, ts);
426
+ }
427
+ else {
428
+ const result = consumeSilentDelta(filter, assistantEvent.delta);
429
+ if (result.kind === "emit" && result.text) {
430
+ startedSilentMessage();
431
+ publishDelta(channelKey, messageIdValue, "text", result.text, ts);
432
+ }
433
+ }
409
434
  }
410
435
  }
436
+ if (storedEvent.type === "tool_execution_start") {
437
+ startedSilentMessage();
438
+ }
411
439
  if (storedEvent.type === "message_end" && storedEvent.role === "assistant") {
440
+ const filter = silentFilters.get(messageIdValue);
441
+ let silent = false;
442
+ if (filter) {
443
+ const final = finalizeSilentFilter(filter);
444
+ silent = final.silent;
445
+ if (!silent) {
446
+ startedSilentMessage();
447
+ if (final.flush) {
448
+ publishDelta(channelKey, messageIdValue, "text", final.flush, ts);
449
+ }
450
+ }
451
+ else {
452
+ pendingMessageStarts.delete(messageIdValue);
453
+ }
454
+ silentFilters.delete(messageIdValue);
455
+ }
456
+ else {
457
+ startedSilentMessage();
458
+ }
412
459
  publish({
413
460
  type: "message_completed",
414
461
  channelKey,
415
462
  messageId: messageIdValue,
416
463
  usage: storedEvent.usage,
464
+ silent: silent || undefined,
417
465
  ts,
418
466
  });
419
467
  }
@@ -450,24 +498,27 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
450
498
  messageId: outboundId,
451
499
  thinkingMs: record.thinkingMs,
452
500
  attachments: webAttachments(config, record.attachments),
501
+ silent: record.silent || undefined,
453
502
  ts: toUnixMs(record.ts),
454
503
  };
455
504
  if (locallyStreamedOutboundIds.delete(outboundId)) {
456
505
  publish(completion);
457
506
  return;
458
507
  }
459
- publish({
460
- type: "message_started",
461
- channelKey: runtime.channelKey,
462
- messageId: outboundId,
463
- role: "assistant",
464
- who: personaName,
465
- ts: toUnixMs(record.ts),
466
- });
467
- if (record.thinking)
468
- publishDelta(runtime.channelKey, outboundId, "thinking", record.thinking, toUnixMs(record.ts));
469
- if (record.text)
470
- publishDelta(runtime.channelKey, outboundId, "text", record.text, toUnixMs(record.ts));
508
+ if (!record.silent) {
509
+ publish({
510
+ type: "message_started",
511
+ channelKey: runtime.channelKey,
512
+ messageId: outboundId,
513
+ role: "assistant",
514
+ who: personaName,
515
+ ts: toUnixMs(record.ts),
516
+ });
517
+ if (record.thinking)
518
+ publishDelta(runtime.channelKey, outboundId, "thinking", record.thinking, toUnixMs(record.ts));
519
+ if (record.text)
520
+ publishDelta(runtime.channelKey, outboundId, "text", record.text, toUnixMs(record.ts));
521
+ }
471
522
  publish(completion);
472
523
  }
473
524
  });
@@ -564,7 +615,9 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
564
615
  finally {
565
616
  await recorder.flush();
566
617
  }
567
- if (!started) {
618
+ const parsed = parseAgentReply(reply.text);
619
+ const finalText = parsed.silent ? "" : reply.text;
620
+ if (!started && !parsed.silent) {
568
621
  publish({
569
622
  type: "message_started",
570
623
  channelKey: runtime.channelKey,
@@ -572,7 +625,9 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
572
625
  role: "assistant",
573
626
  who: personaName,
574
627
  });
575
- publishDelta(runtime.channelKey, assistantMessageId, "text", reply.text);
628
+ if (finalText) {
629
+ publishDelta(runtime.channelKey, assistantMessageId, "text", finalText);
630
+ }
576
631
  }
577
632
  const thinkingMs = thinkingDurationMs(summary);
578
633
  publish({
@@ -581,14 +636,16 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
581
636
  messageId: assistantMessageId,
582
637
  thinkingMs,
583
638
  attachments: webAttachments(config, reply.attachments),
639
+ silent: parsed.silent || undefined,
584
640
  });
585
641
  locallyStreamedOutboundIds.add(assistantMessageId);
586
642
  return {
587
- text: reply.text,
643
+ text: finalText,
588
644
  messageId: assistantMessageId,
589
645
  thinking: summary.thinking,
590
646
  thinkingMs,
591
647
  attachments: reply.attachments,
648
+ silent: parsed.silent,
592
649
  };
593
650
  };
594
651
  const drainJobs = async (runtime) => {
@@ -611,6 +668,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
611
668
  attachments: reply.attachments,
612
669
  thinking: reply.thinking,
613
670
  thinkingMs: reply.thinkingMs,
671
+ silent: reply.silent,
614
672
  replyToMessageId: dispatch.triggerMessageId,
615
673
  });
616
674
  }
@@ -672,7 +730,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
672
730
  }
673
731
  try {
674
732
  if (request.method === "GET" && url.pathname.startsWith("/api/web/attachments/")) {
675
- return serveAttachment(config, response, url.pathname);
733
+ return serveAttachment(config, response, url.pathname, request.headers.range);
676
734
  }
677
735
  if (request.method === "GET" && url.pathname === "/api/web/auth/mode") {
678
736
  sendJson(response, 200, { mode: config.web.authMode, personaName });