openclaw/src/agents/pi-embedded-subscribe.ts

1037 lines
37 KiB
TypeScript
Raw Normal View History

2026-01-09 03:45:14 +00:00
import fs from "node:fs";
import path from "node:path";
2026-01-02 23:37:08 +01:00
import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
2025-12-22 20:45:22 +00:00
import type { AssistantMessage } from "@mariozechner/pi-ai";
import type { AgentSession } from "@mariozechner/pi-coding-agent";
import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js";
2026-01-07 11:08:11 +01:00
import type { ReasoningLevel } from "../auto-reply/thinking.js";
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
2026-01-09 03:45:14 +00:00
import { resolveStateDir } from "../config/paths.js";
2025-12-22 20:45:22 +00:00
import { emitAgentEvent } from "../infra/agent-events.js";
import { createSubsystemLogger } from "../logging.js";
Move provider to a plugin-architecture (#661) * refactor: introduce provider plugin registry * refactor: move provider CLI to plugins * docs: add provider plugin implementation notes * refactor: shift provider runtime logic into plugins * refactor: add plugin defaults and summaries * docs: update provider plugin notes * feat(commands): add /commands slash list * Auto-reply: tidy help message * Auto-reply: fix status command lint * Tests: align google shared expectations * Auto-reply: tidy help message * Auto-reply: fix status command lint * refactor: move provider routing into plugins * test: align agent routing expectations * docs: update provider plugin notes * refactor: route replies via provider plugins * docs: note route-reply plugin hooks * refactor: extend provider plugin contract * refactor: derive provider status from plugins * refactor: unify gateway provider control * refactor: use plugin metadata in auto-reply * fix: parenthesize cron target selection * refactor: derive gateway methods from plugins * refactor: generalize provider logout * refactor: route provider logout through plugins * refactor: move WhatsApp web login methods into plugin * refactor: generalize provider log prefixes * refactor: centralize default chat provider * refactor: derive provider lists from registry * refactor: move provider reload noops into plugins * refactor: resolve web login provider via alias * refactor: derive CLI provider options from plugins * refactor: derive prompt provider list from plugins * style: apply biome lint fixes * fix: resolve provider routing edge cases * docs: update provider plugin refactor notes * fix(gateway): harden agent provider routing * refactor: move provider routing into plugins * refactor: move provider CLI to plugins * refactor: derive provider lists from registry * fix: restore slash command parsing * refactor: align provider ids for schema * refactor: unify outbound target resolution * fix: keep outbound labels stable * feat: add msteams to cron surfaces * fix: clean up lint build issues * refactor: localize chat provider alias normalization * refactor: drive gateway provider lists from plugins * docs: update provider plugin notes * style: format message-provider * fix: avoid provider registry init cycles * style: sort message-provider imports * fix: relax provider alias map typing * refactor: move provider routing into plugins * refactor: add plugin pairing/config adapters * refactor: route pairing and provider removal via plugins * refactor: align auto-reply provider typing * test: stabilize telegram media mocks * docs: update provider plugin refactor notes * refactor: pluginize outbound targets * refactor: pluginize provider selection * refactor: generalize text chunk limits * docs: update provider plugin notes * refactor: generalize group session/config * fix: normalize provider id for room detection * fix: avoid provider init in system prompt * style: formatting cleanup * refactor: normalize agent delivery targets * test: update outbound delivery labels * chore: fix lint regressions * refactor: extend provider plugin adapters * refactor: move elevated/block streaming defaults to plugins * refactor: defer outbound send deps to plugins * docs: note plugin-driven streaming/elevated defaults * refactor: centralize webchat provider constant * refactor: add provider setup adapters * refactor: delegate provider add config to plugins * docs: document plugin-driven provider add * refactor: add plugin state/binding metadata * refactor: build agent provider status from plugins * docs: note plugin-driven agent bindings * refactor: centralize internal provider constant usage * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize default chat provider * refactor: centralize WhatsApp target normalization * refactor: move provider routing into plugins * refactor: normalize agent delivery targets * chore: fix lint regressions * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * feat: expand provider plugin adapters * refactor: route auto-reply via provider plugins * fix: align WhatsApp target normalization * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize WhatsApp target normalization * feat: add /config chat config updates * docs: add /config get alias * feat(commands): add /commands slash list * refactor: centralize default chat provider * style: apply biome lint fixes * chore: fix lint regressions * fix: clean up whatsapp allowlist typing * style: format config command helpers * refactor: pluginize tool threading context * refactor: normalize session announce targets * docs: note new plugin threading and announce hooks * refactor: pluginize message actions * docs: update provider plugin actions notes * fix: align provider action adapters * refactor: centralize webchat checks * style: format message provider helpers * refactor: move provider onboarding into adapters * docs: note onboarding provider adapters * feat: add msteams onboarding adapter * style: organize onboarding imports * fix: normalize msteams allowFrom types * feat: add plugin text chunk limits * refactor: use plugin chunk limit fallbacks * feat: add provider mention stripping hooks * style: organize provider plugin type imports * refactor: generalize health snapshots * refactor: update macOS health snapshot handling * docs: refresh health snapshot notes * style: format health snapshot updates * refactor: drive security warnings via plugins * docs: note provider security adapter * style: format provider security adapters * refactor: centralize provider account defaults * refactor: type gateway client identity constants * chore: regen gateway protocol swift * fix: degrade health on failed provider probe * refactor: centralize pairing approve hint * docs: add plugin CLI command references * refactor: route auth and tool sends through plugins * docs: expand provider plugin hooks * refactor: document provider docking touchpoints * refactor: normalize internal provider defaults * refactor: streamline outbound delivery wiring * refactor: make provider onboarding plugin-owned * refactor: support provider-owned agent tools * refactor: move telegram draft chunking into telegram module * refactor: infer provider tool sends via extractToolSend * fix: repair plugin onboarding imports * refactor: de-dup outbound target normalization * style: tidy plugin and agent imports * refactor: data-drive provider selection line * fix: satisfy lint after provider plugin rebase * test: deflake gateway-cli coverage * style: format gateway-cli coverage test * refactor(provider-plugins): simplify provider ids * test(pairing-cli): avoid provider-specific ternary * style(macos): swiftformat HealthStore * refactor(sandbox): derive provider tool denylist * fix(sandbox): avoid plugin init in defaults * refactor(provider-plugins): centralize provider aliases * style(test): satisfy biome * refactor(protocol): v3 providers.status maps * refactor(ui): adapt to protocol v3 * refactor(macos): adapt to protocol v3 * test: update providers.status v3 fixtures * refactor(gateway): map provider runtime snapshot * test(gateway): update reload runtime snapshot * refactor(whatsapp): normalize heartbeat provider id * docs(refactor): update provider plugin notes * style: satisfy biome after rebase * fix: describe sandboxed elevated in prompt * feat(gateway): add agent image attachments + live probe * refactor: derive CLI provider options from plugins * fix(gateway): harden agent provider routing * fix(gateway): harden agent provider routing * refactor: align provider ids for schema * fix(protocol): keep agent provider string * fix(gateway): harden agent provider routing * fix(protocol): keep agent provider string * refactor: normalize agent delivery targets * refactor: support provider-owned agent tools * refactor(config): provider-keyed elevated allowFrom * style: satisfy biome * fix(gateway): appease provider narrowing * style: satisfy biome * refactor(reply): move group intro hints into plugin * fix(reply): avoid plugin registry init cycle * refactor(providers): add lightweight provider dock * refactor(gateway): use typed client id in connect * refactor(providers): document docks and avoid init cycles * refactor(providers): make media limit helper generic * fix(providers): break plugin registry import cycles * style: satisfy biome * refactor(status-all): build providers table from plugins * refactor(gateway): delegate web login to provider plugin * refactor(provider): drop web alias * refactor(provider): lazy-load monitors * style: satisfy lint/format * style: format status-all providers table * style: swiftformat gateway discovery model * test: make reload plan plugin-driven * fix: avoid token stringification in status-all * refactor: make provider IDs explicit in status * feat: warn on signal/imessage provider runtime errors * test: cover gateway provider runtime warnings in status * fix: add runtime kind to provider status issues * test: cover health degradation on probe failure * fix: keep routeReply lightweight * style: organize routeReply imports * refactor(web): extract auth-store helpers * refactor(whatsapp): lazy login imports * refactor(outbound): route replies via plugin outbound * docs: update provider plugin notes * style: format provider status issues * fix: make sandbox scope warning wrap-safe * refactor: load outbound adapters from provider plugins * docs: update provider plugin outbound notes * style(macos): fix swiftformat lint * docs: changelog for provider plugins * fix(macos): satisfy swiftformat * fix(macos): open settings via menu action * style: format after rebase * fix(macos): open Settings via menu action --------- Co-authored-by: LK <luke@kyohere.com> Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com> Co-authored-by: Xin <xin@imfing.com>
2026-01-11 11:45:25 +00:00
import {
getProviderPlugin,
normalizeProviderId,
} from "../providers/plugins/index.js";
import { truncateUtf16Safe } from "../utils.js";
import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
import {
isMessagingToolDuplicateNormalized,
normalizeTextForComparison,
} from "./pi-embedded-helpers.js";
import {
isMessagingTool,
isMessagingToolSendAction,
type MessagingToolSend,
normalizeTargetForProvider,
} from "./pi-embedded-messaging.js";
2025-12-22 20:45:22 +00:00
import {
extractAssistantText,
extractAssistantThinking,
extractThinkingFromTaggedStream,
extractThinkingFromTaggedText,
formatReasoningMessage,
2025-12-22 20:45:22 +00:00
inferToolMetaFromArgs,
promoteThinkingTagsToBlocks,
2025-12-22 20:45:22 +00:00
} from "./pi-embedded-utils.js";
const THINKING_TAG_SCAN_RE =
/<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
const FINAL_TAG_SCAN_RE = /<\s*(\/?)\s*final\s*>/gi;
2026-01-03 06:16:49 +01:00
const TOOL_RESULT_MAX_CHARS = 8000;
const log = createSubsystemLogger("agent/embedded");
2026-01-09 03:45:14 +00:00
const RAW_STREAM_ENABLED = process.env.CLAWDBOT_RAW_STREAM === "1";
const RAW_STREAM_PATH =
process.env.CLAWDBOT_RAW_STREAM_PATH?.trim() ||
path.join(resolveStateDir(), "logs", "raw-stream.jsonl");
let rawStreamReady = false;
const appendRawStream = (payload: Record<string, unknown>) => {
if (!RAW_STREAM_ENABLED) return;
if (!rawStreamReady) {
rawStreamReady = true;
try {
fs.mkdirSync(path.dirname(RAW_STREAM_PATH), { recursive: true });
} catch {
// ignore raw stream mkdir failures
}
}
try {
void fs.promises.appendFile(
RAW_STREAM_PATH,
`${JSON.stringify(payload)}\n`,
);
} catch {
// ignore raw stream write failures
}
};
2026-01-03 06:16:49 +01:00
export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
2026-01-03 16:45:53 +01:00
2026-01-03 06:16:49 +01:00
function truncateToolText(text: string): string {
if (text.length <= TOOL_RESULT_MAX_CHARS) return text;
return `${truncateUtf16Safe(text, TOOL_RESULT_MAX_CHARS)}\n…(truncated)…`;
2026-01-03 06:16:49 +01:00
}
function sanitizeToolResult(result: unknown): unknown {
if (!result || typeof result !== "object") return result;
const record = result as Record<string, unknown>;
const content = Array.isArray(record.content) ? record.content : null;
if (!content) return record;
const sanitized = content.map((item) => {
if (!item || typeof item !== "object") return item;
const entry = item as Record<string, unknown>;
const type = typeof entry.type === "string" ? entry.type : undefined;
if (type === "text" && typeof entry.text === "string") {
return { ...entry, text: truncateToolText(entry.text) };
}
if (type === "image") {
const data = typeof entry.data === "string" ? entry.data : undefined;
const bytes = data ? data.length : undefined;
const cleaned = { ...entry };
delete cleaned.data;
return { ...cleaned, bytes, omitted: true };
}
return entry;
});
return { ...record, content: sanitized };
}
2025-12-23 14:17:18 +00:00
2026-01-11 10:41:44 +00:00
function isToolResultError(result: unknown): boolean {
if (!result || typeof result !== "object") return false;
const record = result as { details?: unknown };
const details = record.details;
if (!details || typeof details !== "object") return false;
const status = (details as { status?: unknown }).status;
if (typeof status !== "string") return false;
const normalized = status.trim().toLowerCase();
return normalized === "error" || normalized === "timeout";
}
2026-01-08 08:49:16 +01:00
function extractMessagingToolSend(
toolName: string,
args: Record<string, unknown>,
): MessagingToolSend | undefined {
Move provider to a plugin-architecture (#661) * refactor: introduce provider plugin registry * refactor: move provider CLI to plugins * docs: add provider plugin implementation notes * refactor: shift provider runtime logic into plugins * refactor: add plugin defaults and summaries * docs: update provider plugin notes * feat(commands): add /commands slash list * Auto-reply: tidy help message * Auto-reply: fix status command lint * Tests: align google shared expectations * Auto-reply: tidy help message * Auto-reply: fix status command lint * refactor: move provider routing into plugins * test: align agent routing expectations * docs: update provider plugin notes * refactor: route replies via provider plugins * docs: note route-reply plugin hooks * refactor: extend provider plugin contract * refactor: derive provider status from plugins * refactor: unify gateway provider control * refactor: use plugin metadata in auto-reply * fix: parenthesize cron target selection * refactor: derive gateway methods from plugins * refactor: generalize provider logout * refactor: route provider logout through plugins * refactor: move WhatsApp web login methods into plugin * refactor: generalize provider log prefixes * refactor: centralize default chat provider * refactor: derive provider lists from registry * refactor: move provider reload noops into plugins * refactor: resolve web login provider via alias * refactor: derive CLI provider options from plugins * refactor: derive prompt provider list from plugins * style: apply biome lint fixes * fix: resolve provider routing edge cases * docs: update provider plugin refactor notes * fix(gateway): harden agent provider routing * refactor: move provider routing into plugins * refactor: move provider CLI to plugins * refactor: derive provider lists from registry * fix: restore slash command parsing * refactor: align provider ids for schema * refactor: unify outbound target resolution * fix: keep outbound labels stable * feat: add msteams to cron surfaces * fix: clean up lint build issues * refactor: localize chat provider alias normalization * refactor: drive gateway provider lists from plugins * docs: update provider plugin notes * style: format message-provider * fix: avoid provider registry init cycles * style: sort message-provider imports * fix: relax provider alias map typing * refactor: move provider routing into plugins * refactor: add plugin pairing/config adapters * refactor: route pairing and provider removal via plugins * refactor: align auto-reply provider typing * test: stabilize telegram media mocks * docs: update provider plugin refactor notes * refactor: pluginize outbound targets * refactor: pluginize provider selection * refactor: generalize text chunk limits * docs: update provider plugin notes * refactor: generalize group session/config * fix: normalize provider id for room detection * fix: avoid provider init in system prompt * style: formatting cleanup * refactor: normalize agent delivery targets * test: update outbound delivery labels * chore: fix lint regressions * refactor: extend provider plugin adapters * refactor: move elevated/block streaming defaults to plugins * refactor: defer outbound send deps to plugins * docs: note plugin-driven streaming/elevated defaults * refactor: centralize webchat provider constant * refactor: add provider setup adapters * refactor: delegate provider add config to plugins * docs: document plugin-driven provider add * refactor: add plugin state/binding metadata * refactor: build agent provider status from plugins * docs: note plugin-driven agent bindings * refactor: centralize internal provider constant usage * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize default chat provider * refactor: centralize WhatsApp target normalization * refactor: move provider routing into plugins * refactor: normalize agent delivery targets * chore: fix lint regressions * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * feat: expand provider plugin adapters * refactor: route auto-reply via provider plugins * fix: align WhatsApp target normalization * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize WhatsApp target normalization * feat: add /config chat config updates * docs: add /config get alias * feat(commands): add /commands slash list * refactor: centralize default chat provider * style: apply biome lint fixes * chore: fix lint regressions * fix: clean up whatsapp allowlist typing * style: format config command helpers * refactor: pluginize tool threading context * refactor: normalize session announce targets * docs: note new plugin threading and announce hooks * refactor: pluginize message actions * docs: update provider plugin actions notes * fix: align provider action adapters * refactor: centralize webchat checks * style: format message provider helpers * refactor: move provider onboarding into adapters * docs: note onboarding provider adapters * feat: add msteams onboarding adapter * style: organize onboarding imports * fix: normalize msteams allowFrom types * feat: add plugin text chunk limits * refactor: use plugin chunk limit fallbacks * feat: add provider mention stripping hooks * style: organize provider plugin type imports * refactor: generalize health snapshots * refactor: update macOS health snapshot handling * docs: refresh health snapshot notes * style: format health snapshot updates * refactor: drive security warnings via plugins * docs: note provider security adapter * style: format provider security adapters * refactor: centralize provider account defaults * refactor: type gateway client identity constants * chore: regen gateway protocol swift * fix: degrade health on failed provider probe * refactor: centralize pairing approve hint * docs: add plugin CLI command references * refactor: route auth and tool sends through plugins * docs: expand provider plugin hooks * refactor: document provider docking touchpoints * refactor: normalize internal provider defaults * refactor: streamline outbound delivery wiring * refactor: make provider onboarding plugin-owned * refactor: support provider-owned agent tools * refactor: move telegram draft chunking into telegram module * refactor: infer provider tool sends via extractToolSend * fix: repair plugin onboarding imports * refactor: de-dup outbound target normalization * style: tidy plugin and agent imports * refactor: data-drive provider selection line * fix: satisfy lint after provider plugin rebase * test: deflake gateway-cli coverage * style: format gateway-cli coverage test * refactor(provider-plugins): simplify provider ids * test(pairing-cli): avoid provider-specific ternary * style(macos): swiftformat HealthStore * refactor(sandbox): derive provider tool denylist * fix(sandbox): avoid plugin init in defaults * refactor(provider-plugins): centralize provider aliases * style(test): satisfy biome * refactor(protocol): v3 providers.status maps * refactor(ui): adapt to protocol v3 * refactor(macos): adapt to protocol v3 * test: update providers.status v3 fixtures * refactor(gateway): map provider runtime snapshot * test(gateway): update reload runtime snapshot * refactor(whatsapp): normalize heartbeat provider id * docs(refactor): update provider plugin notes * style: satisfy biome after rebase * fix: describe sandboxed elevated in prompt * feat(gateway): add agent image attachments + live probe * refactor: derive CLI provider options from plugins * fix(gateway): harden agent provider routing * fix(gateway): harden agent provider routing * refactor: align provider ids for schema * fix(protocol): keep agent provider string * fix(gateway): harden agent provider routing * fix(protocol): keep agent provider string * refactor: normalize agent delivery targets * refactor: support provider-owned agent tools * refactor(config): provider-keyed elevated allowFrom * style: satisfy biome * fix(gateway): appease provider narrowing * style: satisfy biome * refactor(reply): move group intro hints into plugin * fix(reply): avoid plugin registry init cycle * refactor(providers): add lightweight provider dock * refactor(gateway): use typed client id in connect * refactor(providers): document docks and avoid init cycles * refactor(providers): make media limit helper generic * fix(providers): break plugin registry import cycles * style: satisfy biome * refactor(status-all): build providers table from plugins * refactor(gateway): delegate web login to provider plugin * refactor(provider): drop web alias * refactor(provider): lazy-load monitors * style: satisfy lint/format * style: format status-all providers table * style: swiftformat gateway discovery model * test: make reload plan plugin-driven * fix: avoid token stringification in status-all * refactor: make provider IDs explicit in status * feat: warn on signal/imessage provider runtime errors * test: cover gateway provider runtime warnings in status * fix: add runtime kind to provider status issues * test: cover health degradation on probe failure * fix: keep routeReply lightweight * style: organize routeReply imports * refactor(web): extract auth-store helpers * refactor(whatsapp): lazy login imports * refactor(outbound): route replies via plugin outbound * docs: update provider plugin notes * style: format provider status issues * fix: make sandbox scope warning wrap-safe * refactor: load outbound adapters from provider plugins * docs: update provider plugin outbound notes * style(macos): fix swiftformat lint * docs: changelog for provider plugins * fix(macos): satisfy swiftformat * fix(macos): open settings via menu action * style: format after rebase * fix(macos): open Settings via menu action --------- Co-authored-by: LK <luke@kyohere.com> Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com> Co-authored-by: Xin <xin@imfing.com>
2026-01-11 11:45:25 +00:00
// Provider docking: new provider tools must implement plugin.actions.extractToolSend.
2026-01-08 08:49:16 +01:00
const action = typeof args.action === "string" ? args.action.trim() : "";
const accountIdRaw =
typeof args.accountId === "string" ? args.accountId.trim() : undefined;
const accountId = accountIdRaw ? accountIdRaw : undefined;
2026-01-10 13:38:28 +01:00
if (toolName === "message") {
if (action !== "send" && action !== "thread-reply") return undefined;
const toRaw = typeof args.to === "string" ? args.to : undefined;
if (!toRaw) return undefined;
const providerRaw =
typeof args.provider === "string" ? args.provider.trim() : "";
Move provider to a plugin-architecture (#661) * refactor: introduce provider plugin registry * refactor: move provider CLI to plugins * docs: add provider plugin implementation notes * refactor: shift provider runtime logic into plugins * refactor: add plugin defaults and summaries * docs: update provider plugin notes * feat(commands): add /commands slash list * Auto-reply: tidy help message * Auto-reply: fix status command lint * Tests: align google shared expectations * Auto-reply: tidy help message * Auto-reply: fix status command lint * refactor: move provider routing into plugins * test: align agent routing expectations * docs: update provider plugin notes * refactor: route replies via provider plugins * docs: note route-reply plugin hooks * refactor: extend provider plugin contract * refactor: derive provider status from plugins * refactor: unify gateway provider control * refactor: use plugin metadata in auto-reply * fix: parenthesize cron target selection * refactor: derive gateway methods from plugins * refactor: generalize provider logout * refactor: route provider logout through plugins * refactor: move WhatsApp web login methods into plugin * refactor: generalize provider log prefixes * refactor: centralize default chat provider * refactor: derive provider lists from registry * refactor: move provider reload noops into plugins * refactor: resolve web login provider via alias * refactor: derive CLI provider options from plugins * refactor: derive prompt provider list from plugins * style: apply biome lint fixes * fix: resolve provider routing edge cases * docs: update provider plugin refactor notes * fix(gateway): harden agent provider routing * refactor: move provider routing into plugins * refactor: move provider CLI to plugins * refactor: derive provider lists from registry * fix: restore slash command parsing * refactor: align provider ids for schema * refactor: unify outbound target resolution * fix: keep outbound labels stable * feat: add msteams to cron surfaces * fix: clean up lint build issues * refactor: localize chat provider alias normalization * refactor: drive gateway provider lists from plugins * docs: update provider plugin notes * style: format message-provider * fix: avoid provider registry init cycles * style: sort message-provider imports * fix: relax provider alias map typing * refactor: move provider routing into plugins * refactor: add plugin pairing/config adapters * refactor: route pairing and provider removal via plugins * refactor: align auto-reply provider typing * test: stabilize telegram media mocks * docs: update provider plugin refactor notes * refactor: pluginize outbound targets * refactor: pluginize provider selection * refactor: generalize text chunk limits * docs: update provider plugin notes * refactor: generalize group session/config * fix: normalize provider id for room detection * fix: avoid provider init in system prompt * style: formatting cleanup * refactor: normalize agent delivery targets * test: update outbound delivery labels * chore: fix lint regressions * refactor: extend provider plugin adapters * refactor: move elevated/block streaming defaults to plugins * refactor: defer outbound send deps to plugins * docs: note plugin-driven streaming/elevated defaults * refactor: centralize webchat provider constant * refactor: add provider setup adapters * refactor: delegate provider add config to plugins * docs: document plugin-driven provider add * refactor: add plugin state/binding metadata * refactor: build agent provider status from plugins * docs: note plugin-driven agent bindings * refactor: centralize internal provider constant usage * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize default chat provider * refactor: centralize WhatsApp target normalization * refactor: move provider routing into plugins * refactor: normalize agent delivery targets * chore: fix lint regressions * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * feat: expand provider plugin adapters * refactor: route auto-reply via provider plugins * fix: align WhatsApp target normalization * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize WhatsApp target normalization * feat: add /config chat config updates * docs: add /config get alias * feat(commands): add /commands slash list * refactor: centralize default chat provider * style: apply biome lint fixes * chore: fix lint regressions * fix: clean up whatsapp allowlist typing * style: format config command helpers * refactor: pluginize tool threading context * refactor: normalize session announce targets * docs: note new plugin threading and announce hooks * refactor: pluginize message actions * docs: update provider plugin actions notes * fix: align provider action adapters * refactor: centralize webchat checks * style: format message provider helpers * refactor: move provider onboarding into adapters * docs: note onboarding provider adapters * feat: add msteams onboarding adapter * style: organize onboarding imports * fix: normalize msteams allowFrom types * feat: add plugin text chunk limits * refactor: use plugin chunk limit fallbacks * feat: add provider mention stripping hooks * style: organize provider plugin type imports * refactor: generalize health snapshots * refactor: update macOS health snapshot handling * docs: refresh health snapshot notes * style: format health snapshot updates * refactor: drive security warnings via plugins * docs: note provider security adapter * style: format provider security adapters * refactor: centralize provider account defaults * refactor: type gateway client identity constants * chore: regen gateway protocol swift * fix: degrade health on failed provider probe * refactor: centralize pairing approve hint * docs: add plugin CLI command references * refactor: route auth and tool sends through plugins * docs: expand provider plugin hooks * refactor: document provider docking touchpoints * refactor: normalize internal provider defaults * refactor: streamline outbound delivery wiring * refactor: make provider onboarding plugin-owned * refactor: support provider-owned agent tools * refactor: move telegram draft chunking into telegram module * refactor: infer provider tool sends via extractToolSend * fix: repair plugin onboarding imports * refactor: de-dup outbound target normalization * style: tidy plugin and agent imports * refactor: data-drive provider selection line * fix: satisfy lint after provider plugin rebase * test: deflake gateway-cli coverage * style: format gateway-cli coverage test * refactor(provider-plugins): simplify provider ids * test(pairing-cli): avoid provider-specific ternary * style(macos): swiftformat HealthStore * refactor(sandbox): derive provider tool denylist * fix(sandbox): avoid plugin init in defaults * refactor(provider-plugins): centralize provider aliases * style(test): satisfy biome * refactor(protocol): v3 providers.status maps * refactor(ui): adapt to protocol v3 * refactor(macos): adapt to protocol v3 * test: update providers.status v3 fixtures * refactor(gateway): map provider runtime snapshot * test(gateway): update reload runtime snapshot * refactor(whatsapp): normalize heartbeat provider id * docs(refactor): update provider plugin notes * style: satisfy biome after rebase * fix: describe sandboxed elevated in prompt * feat(gateway): add agent image attachments + live probe * refactor: derive CLI provider options from plugins * fix(gateway): harden agent provider routing * fix(gateway): harden agent provider routing * refactor: align provider ids for schema * fix(protocol): keep agent provider string * fix(gateway): harden agent provider routing * fix(protocol): keep agent provider string * refactor: normalize agent delivery targets * refactor: support provider-owned agent tools * refactor(config): provider-keyed elevated allowFrom * style: satisfy biome * fix(gateway): appease provider narrowing * style: satisfy biome * refactor(reply): move group intro hints into plugin * fix(reply): avoid plugin registry init cycle * refactor(providers): add lightweight provider dock * refactor(gateway): use typed client id in connect * refactor(providers): document docks and avoid init cycles * refactor(providers): make media limit helper generic * fix(providers): break plugin registry import cycles * style: satisfy biome * refactor(status-all): build providers table from plugins * refactor(gateway): delegate web login to provider plugin * refactor(provider): drop web alias * refactor(provider): lazy-load monitors * style: satisfy lint/format * style: format status-all providers table * style: swiftformat gateway discovery model * test: make reload plan plugin-driven * fix: avoid token stringification in status-all * refactor: make provider IDs explicit in status * feat: warn on signal/imessage provider runtime errors * test: cover gateway provider runtime warnings in status * fix: add runtime kind to provider status issues * test: cover health degradation on probe failure * fix: keep routeReply lightweight * style: organize routeReply imports * refactor(web): extract auth-store helpers * refactor(whatsapp): lazy login imports * refactor(outbound): route replies via plugin outbound * docs: update provider plugin notes * style: format provider status issues * fix: make sandbox scope warning wrap-safe * refactor: load outbound adapters from provider plugins * docs: update provider plugin outbound notes * style(macos): fix swiftformat lint * docs: changelog for provider plugins * fix(macos): satisfy swiftformat * fix(macos): open settings via menu action * style: format after rebase * fix(macos): open Settings via menu action --------- Co-authored-by: LK <luke@kyohere.com> Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com> Co-authored-by: Xin <xin@imfing.com>
2026-01-11 11:45:25 +00:00
const providerId = providerRaw ? normalizeProviderId(providerRaw) : null;
const provider =
providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message");
const to = normalizeTargetForProvider(provider, toRaw);
2026-01-10 13:38:28 +01:00
return to ? { tool: toolName, provider, accountId, to } : undefined;
}
Move provider to a plugin-architecture (#661) * refactor: introduce provider plugin registry * refactor: move provider CLI to plugins * docs: add provider plugin implementation notes * refactor: shift provider runtime logic into plugins * refactor: add plugin defaults and summaries * docs: update provider plugin notes * feat(commands): add /commands slash list * Auto-reply: tidy help message * Auto-reply: fix status command lint * Tests: align google shared expectations * Auto-reply: tidy help message * Auto-reply: fix status command lint * refactor: move provider routing into plugins * test: align agent routing expectations * docs: update provider plugin notes * refactor: route replies via provider plugins * docs: note route-reply plugin hooks * refactor: extend provider plugin contract * refactor: derive provider status from plugins * refactor: unify gateway provider control * refactor: use plugin metadata in auto-reply * fix: parenthesize cron target selection * refactor: derive gateway methods from plugins * refactor: generalize provider logout * refactor: route provider logout through plugins * refactor: move WhatsApp web login methods into plugin * refactor: generalize provider log prefixes * refactor: centralize default chat provider * refactor: derive provider lists from registry * refactor: move provider reload noops into plugins * refactor: resolve web login provider via alias * refactor: derive CLI provider options from plugins * refactor: derive prompt provider list from plugins * style: apply biome lint fixes * fix: resolve provider routing edge cases * docs: update provider plugin refactor notes * fix(gateway): harden agent provider routing * refactor: move provider routing into plugins * refactor: move provider CLI to plugins * refactor: derive provider lists from registry * fix: restore slash command parsing * refactor: align provider ids for schema * refactor: unify outbound target resolution * fix: keep outbound labels stable * feat: add msteams to cron surfaces * fix: clean up lint build issues * refactor: localize chat provider alias normalization * refactor: drive gateway provider lists from plugins * docs: update provider plugin notes * style: format message-provider * fix: avoid provider registry init cycles * style: sort message-provider imports * fix: relax provider alias map typing * refactor: move provider routing into plugins * refactor: add plugin pairing/config adapters * refactor: route pairing and provider removal via plugins * refactor: align auto-reply provider typing * test: stabilize telegram media mocks * docs: update provider plugin refactor notes * refactor: pluginize outbound targets * refactor: pluginize provider selection * refactor: generalize text chunk limits * docs: update provider plugin notes * refactor: generalize group session/config * fix: normalize provider id for room detection * fix: avoid provider init in system prompt * style: formatting cleanup * refactor: normalize agent delivery targets * test: update outbound delivery labels * chore: fix lint regressions * refactor: extend provider plugin adapters * refactor: move elevated/block streaming defaults to plugins * refactor: defer outbound send deps to plugins * docs: note plugin-driven streaming/elevated defaults * refactor: centralize webchat provider constant * refactor: add provider setup adapters * refactor: delegate provider add config to plugins * docs: document plugin-driven provider add * refactor: add plugin state/binding metadata * refactor: build agent provider status from plugins * docs: note plugin-driven agent bindings * refactor: centralize internal provider constant usage * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize default chat provider * refactor: centralize WhatsApp target normalization * refactor: move provider routing into plugins * refactor: normalize agent delivery targets * chore: fix lint regressions * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * feat: expand provider plugin adapters * refactor: route auto-reply via provider plugins * fix: align WhatsApp target normalization * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize WhatsApp target normalization * feat: add /config chat config updates * docs: add /config get alias * feat(commands): add /commands slash list * refactor: centralize default chat provider * style: apply biome lint fixes * chore: fix lint regressions * fix: clean up whatsapp allowlist typing * style: format config command helpers * refactor: pluginize tool threading context * refactor: normalize session announce targets * docs: note new plugin threading and announce hooks * refactor: pluginize message actions * docs: update provider plugin actions notes * fix: align provider action adapters * refactor: centralize webchat checks * style: format message provider helpers * refactor: move provider onboarding into adapters * docs: note onboarding provider adapters * feat: add msteams onboarding adapter * style: organize onboarding imports * fix: normalize msteams allowFrom types * feat: add plugin text chunk limits * refactor: use plugin chunk limit fallbacks * feat: add provider mention stripping hooks * style: organize provider plugin type imports * refactor: generalize health snapshots * refactor: update macOS health snapshot handling * docs: refresh health snapshot notes * style: format health snapshot updates * refactor: drive security warnings via plugins * docs: note provider security adapter * style: format provider security adapters * refactor: centralize provider account defaults * refactor: type gateway client identity constants * chore: regen gateway protocol swift * fix: degrade health on failed provider probe * refactor: centralize pairing approve hint * docs: add plugin CLI command references * refactor: route auth and tool sends through plugins * docs: expand provider plugin hooks * refactor: document provider docking touchpoints * refactor: normalize internal provider defaults * refactor: streamline outbound delivery wiring * refactor: make provider onboarding plugin-owned * refactor: support provider-owned agent tools * refactor: move telegram draft chunking into telegram module * refactor: infer provider tool sends via extractToolSend * fix: repair plugin onboarding imports * refactor: de-dup outbound target normalization * style: tidy plugin and agent imports * refactor: data-drive provider selection line * fix: satisfy lint after provider plugin rebase * test: deflake gateway-cli coverage * style: format gateway-cli coverage test * refactor(provider-plugins): simplify provider ids * test(pairing-cli): avoid provider-specific ternary * style(macos): swiftformat HealthStore * refactor(sandbox): derive provider tool denylist * fix(sandbox): avoid plugin init in defaults * refactor(provider-plugins): centralize provider aliases * style(test): satisfy biome * refactor(protocol): v3 providers.status maps * refactor(ui): adapt to protocol v3 * refactor(macos): adapt to protocol v3 * test: update providers.status v3 fixtures * refactor(gateway): map provider runtime snapshot * test(gateway): update reload runtime snapshot * refactor(whatsapp): normalize heartbeat provider id * docs(refactor): update provider plugin notes * style: satisfy biome after rebase * fix: describe sandboxed elevated in prompt * feat(gateway): add agent image attachments + live probe * refactor: derive CLI provider options from plugins * fix(gateway): harden agent provider routing * fix(gateway): harden agent provider routing * refactor: align provider ids for schema * fix(protocol): keep agent provider string * fix(gateway): harden agent provider routing * fix(protocol): keep agent provider string * refactor: normalize agent delivery targets * refactor: support provider-owned agent tools * refactor(config): provider-keyed elevated allowFrom * style: satisfy biome * fix(gateway): appease provider narrowing * style: satisfy biome * refactor(reply): move group intro hints into plugin * fix(reply): avoid plugin registry init cycle * refactor(providers): add lightweight provider dock * refactor(gateway): use typed client id in connect * refactor(providers): document docks and avoid init cycles * refactor(providers): make media limit helper generic * fix(providers): break plugin registry import cycles * style: satisfy biome * refactor(status-all): build providers table from plugins * refactor(gateway): delegate web login to provider plugin * refactor(provider): drop web alias * refactor(provider): lazy-load monitors * style: satisfy lint/format * style: format status-all providers table * style: swiftformat gateway discovery model * test: make reload plan plugin-driven * fix: avoid token stringification in status-all * refactor: make provider IDs explicit in status * feat: warn on signal/imessage provider runtime errors * test: cover gateway provider runtime warnings in status * fix: add runtime kind to provider status issues * test: cover health degradation on probe failure * fix: keep routeReply lightweight * style: organize routeReply imports * refactor(web): extract auth-store helpers * refactor(whatsapp): lazy login imports * refactor(outbound): route replies via plugin outbound * docs: update provider plugin notes * style: format provider status issues * fix: make sandbox scope warning wrap-safe * refactor: load outbound adapters from provider plugins * docs: update provider plugin outbound notes * style(macos): fix swiftformat lint * docs: changelog for provider plugins * fix(macos): satisfy swiftformat * fix(macos): open settings via menu action * style: format after rebase * fix(macos): open Settings via menu action --------- Co-authored-by: LK <luke@kyohere.com> Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com> Co-authored-by: Xin <xin@imfing.com>
2026-01-11 11:45:25 +00:00
const providerId = normalizeProviderId(toolName);
if (!providerId) return undefined;
const plugin = getProviderPlugin(providerId);
const extracted = plugin?.actions?.extractToolSend?.({ args });
if (!extracted?.to) return undefined;
const to = normalizeTargetForProvider(providerId, extracted.to);
return to
? {
tool: toolName,
provider: providerId,
accountId: extracted.accountId ?? accountId,
to,
}
: undefined;
2026-01-08 08:49:16 +01:00
}
2025-12-22 20:45:22 +00:00
export function subscribeEmbeddedPiSession(params: {
session: AgentSession;
runId: string;
verboseLevel?: "off" | "on";
2026-01-07 11:08:11 +01:00
reasoningMode?: ReasoningLevel;
2025-12-22 20:45:22 +00:00
shouldEmitToolResult?: () => boolean;
onToolResult?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
2026-01-07 11:08:11 +01:00
onReasoningStream?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
2026-01-03 00:28:33 +01:00
onBlockReply?: (payload: {
text?: string;
mediaUrls?: string[];
audioAsVoice?: boolean;
2026-01-03 00:28:33 +01:00
}) => void | Promise<void>;
/** Flush pending block replies (e.g., before tool execution to preserve message boundaries). */
onBlockReplyFlush?: () => void | Promise<void>;
blockReplyBreak?: "text_end" | "message_end";
2026-01-03 16:45:53 +01:00
blockReplyChunking?: BlockReplyChunking;
2025-12-22 20:45:22 +00:00
onPartialReply?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
onAgentEvent?: (evt: {
stream: string;
data: Record<string, unknown>;
}) => void;
enforceFinalTag?: boolean;
2025-12-22 20:45:22 +00:00
}) {
const assistantTexts: string[] = [];
const toolMetas: Array<{ toolName?: string; meta?: string }> = [];
const toolMetaById = new Map<string, string | undefined>();
const toolSummaryById = new Set<string>();
const blockReplyBreak = params.blockReplyBreak ?? "text_end";
2026-01-07 11:08:11 +01:00
const reasoningMode = params.reasoningMode ?? "off";
const includeReasoning = reasoningMode === "on";
const shouldEmitPartialReplies = !(includeReasoning && !params.onBlockReply);
2026-01-07 11:08:11 +01:00
const streamReasoning =
reasoningMode === "stream" &&
typeof params.onReasoningStream === "function";
2025-12-22 20:45:22 +00:00
let deltaBuffer = "";
2026-01-03 16:45:53 +01:00
let blockBuffer = "";
// Track if a streamed chunk opened a <think> block (stateful across chunks).
const blockState = { thinking: false, final: false };
2025-12-22 20:45:22 +00:00
let lastStreamedAssistant: string | undefined;
2026-01-07 11:08:11 +01:00
let lastStreamedReasoning: string | undefined;
2026-01-03 01:48:38 +01:00
let lastBlockReplyText: string | undefined;
let assistantTextBaseline = 0;
let suppressBlockChunks = false; // Avoid late chunk inserts after final text merge.
2025-12-26 10:16:50 +01:00
let compactionInFlight = false;
let pendingCompactionRetry = 0;
let compactionRetryResolve: (() => void) | undefined;
let compactionRetryPromise: Promise<void> | null = null;
let lastReasoningSent: string | undefined;
2025-12-26 10:16:50 +01:00
const resetAssistantMessageState = (nextAssistantTextBaseline: number) => {
deltaBuffer = "";
blockBuffer = "";
blockChunker?.reset();
blockState.thinking = false;
blockState.final = false;
lastStreamedAssistant = undefined;
lastBlockReplyText = undefined;
lastStreamedReasoning = undefined;
lastReasoningSent = undefined;
suppressBlockChunks = false;
assistantTextBaseline = nextAssistantTextBaseline;
};
const finalizeAssistantTexts = (args: {
text: string;
addedDuringMessage: boolean;
chunkerHasBuffered: boolean;
}) => {
const { text, addedDuringMessage, chunkerHasBuffered } = args;
// If we're not streaming block replies, ensure the final payload includes
// the final text even when interim streaming was enabled.
if (includeReasoning && text && !params.onBlockReply) {
if (assistantTexts.length > assistantTextBaseline) {
assistantTexts.splice(
assistantTextBaseline,
assistantTexts.length - assistantTextBaseline,
text,
);
} else {
const last = assistantTexts.at(-1);
if (!last || last !== text) assistantTexts.push(text);
}
suppressBlockChunks = true;
} else if (!addedDuringMessage && !chunkerHasBuffered && text) {
// Non-streaming models (no text_delta): ensure assistantTexts gets the final
// text when the chunker has nothing buffered to drain.
const last = assistantTexts.at(-1);
if (!last || last !== text) assistantTexts.push(text);
}
assistantTextBaseline = assistantTexts.length;
};
// ── Messaging tool duplicate detection ──────────────────────────────────────
// Track texts sent via messaging tools to suppress duplicate block replies.
// Only committed (successful) texts are checked - pending texts are tracked
// to support commit logic but not used for suppression (avoiding lost messages on tool failure).
// These tools can send messages via sendMessage/threadReply actions (or sessions_send with message).
const MAX_MESSAGING_SENT_TEXTS = 200;
const MAX_MESSAGING_SENT_TARGETS = 200;
const messagingToolSentTexts: string[] = [];
const messagingToolSentTextsNormalized: string[] = [];
2026-01-08 08:49:16 +01:00
const messagingToolSentTargets: MessagingToolSend[] = [];
const pendingMessagingTexts = new Map<string, string>();
2026-01-08 08:49:16 +01:00
const pendingMessagingTargets = new Map<string, MessagingToolSend>();
const trimMessagingToolSent = () => {
if (messagingToolSentTexts.length > MAX_MESSAGING_SENT_TEXTS) {
const overflow = messagingToolSentTexts.length - MAX_MESSAGING_SENT_TEXTS;
messagingToolSentTexts.splice(0, overflow);
messagingToolSentTextsNormalized.splice(0, overflow);
}
if (messagingToolSentTargets.length > MAX_MESSAGING_SENT_TARGETS) {
const overflow =
messagingToolSentTargets.length - MAX_MESSAGING_SENT_TARGETS;
messagingToolSentTargets.splice(0, overflow);
}
};
2025-12-26 10:16:50 +01:00
const ensureCompactionPromise = () => {
if (!compactionRetryPromise) {
compactionRetryPromise = new Promise((resolve) => {
compactionRetryResolve = resolve;
});
}
};
const noteCompactionRetry = () => {
pendingCompactionRetry += 1;
ensureCompactionPromise();
};
const resolveCompactionRetry = () => {
if (pendingCompactionRetry <= 0) return;
pendingCompactionRetry -= 1;
if (pendingCompactionRetry === 0 && !compactionInFlight) {
compactionRetryResolve?.();
compactionRetryResolve = undefined;
compactionRetryPromise = null;
}
};
const maybeResolveCompactionWait = () => {
if (pendingCompactionRetry === 0 && !compactionInFlight) {
compactionRetryResolve?.();
compactionRetryResolve = undefined;
compactionRetryPromise = null;
}
};
2025-12-22 20:45:22 +00:00
2026-01-03 16:45:53 +01:00
const blockChunking = params.blockReplyChunking;
const blockChunker = blockChunking
? new EmbeddedBlockChunker(blockChunking)
: null;
// KNOWN: Provider streams are not strictly once-only or perfectly ordered.
// `text_end` can repeat full content; late `text_end` can arrive after `message_end`.
// Tests: `src/agents/pi-embedded-subscribe.test.ts` (e.g. late text_end cases).
const shouldEmitToolResult = () =>
typeof params.shouldEmitToolResult === "function"
? params.shouldEmitToolResult()
: params.verboseLevel === "on";
const emitToolSummary = (toolName?: string, meta?: string) => {
if (!params.onToolResult) return;
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined);
const { text: cleanedText, mediaUrls } = parseReplyDirectives(agg);
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) return;
try {
void params.onToolResult({
text: cleanedText,
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
});
} catch {
// ignore tool result delivery failures
}
};
2026-01-03 16:45:53 +01:00
const stripBlockTags = (
text: string,
state: { thinking: boolean; final: boolean },
): string => {
if (!text) return text;
// 1. Handle <think> blocks (stateful, strip content inside)
let processed = "";
THINKING_TAG_SCAN_RE.lastIndex = 0;
let lastIndex = 0;
let inThinking = state.thinking;
for (const match of text.matchAll(THINKING_TAG_SCAN_RE)) {
const idx = match.index ?? 0;
if (!inThinking) {
processed += text.slice(lastIndex, idx);
}
const isClose = match[1] === "/";
inThinking = !isClose;
lastIndex = idx + match[0].length;
}
if (!inThinking) {
processed += text.slice(lastIndex);
}
state.thinking = inThinking;
// 2. Handle <final> blocks (stateful, strip content OUTSIDE)
// If enforcement is disabled, we still strip the tags themselves to prevent
// hallucinations (e.g. Minimax copying the style) from leaking, but we
// do not enforce buffering/extraction logic.
if (!params.enforceFinalTag) {
FINAL_TAG_SCAN_RE.lastIndex = 0;
return processed.replace(FINAL_TAG_SCAN_RE, "");
}
// If enforcement is enabled, only return text that appeared inside a <final> block.
let result = "";
FINAL_TAG_SCAN_RE.lastIndex = 0;
let lastFinalIndex = 0;
let inFinal = state.final;
let everInFinal = state.final;
for (const match of processed.matchAll(FINAL_TAG_SCAN_RE)) {
const idx = match.index ?? 0;
const isClose = match[1] === "/";
if (!inFinal && !isClose) {
// Found <final> start tag.
inFinal = true;
everInFinal = true;
lastFinalIndex = idx + match[0].length;
} else if (inFinal && isClose) {
// Found </final> end tag.
result += processed.slice(lastFinalIndex, idx);
inFinal = false;
lastFinalIndex = idx + match[0].length;
}
}
if (inFinal) {
result += processed.slice(lastFinalIndex);
}
state.final = inFinal;
// Strict Mode: If enforcing final tags, we MUST NOT return content unless
// we have seen a <final> tag. Otherwise, we leak "thinking out loud" text
// (e.g. "**Locating Manulife**...") that the model emitted without <think> tags.
if (!everInFinal) {
return "";
}
// Hardened Cleanup: Remove any remaining <final> tags that might have been
// missed (e.g. nested tags or hallucinations) to prevent leakage.
return result.replace(FINAL_TAG_SCAN_RE, "");
};
2026-01-03 16:45:53 +01:00
const emitBlockChunk = (text: string) => {
if (suppressBlockChunks) return;
// Strip <think> and <final> blocks across chunk boundaries to avoid leaking reasoning.
const chunk = stripBlockTags(text, blockState).trimEnd();
2026-01-03 16:45:53 +01:00
if (!chunk) return;
if (chunk === lastBlockReplyText) return;
// Only check committed (successful) messaging tool texts - checking pending texts
// is risky because if the tool fails after suppression, the user gets no response
const normalizedChunk = normalizeTextForComparison(chunk);
if (
isMessagingToolDuplicateNormalized(
normalizedChunk,
messagingToolSentTextsNormalized,
)
) {
log.debug(
`Skipping block reply - already sent via messaging tool: ${chunk.slice(0, 50)}...`,
);
return;
}
2026-01-03 16:45:53 +01:00
lastBlockReplyText = chunk;
assistantTexts.push(chunk);
if (!params.onBlockReply) return;
const splitResult = parseReplyDirectives(chunk);
const { text: cleanedText, mediaUrls, audioAsVoice } = splitResult;
// Skip empty payloads, but always emit if audioAsVoice is set (to propagate the flag)
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0) && !audioAsVoice)
return;
2026-01-03 16:45:53 +01:00
void params.onBlockReply({
text: cleanedText,
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
audioAsVoice,
2026-01-03 16:45:53 +01:00
});
};
const flushBlockReplyBuffer = () => {
if (!params.onBlockReply) return;
if (blockChunker?.hasBuffered()) {
blockChunker.drain({ force: true, emit: emitBlockChunk });
blockChunker.reset();
return;
}
if (blockBuffer.length > 0) {
emitBlockChunk(blockBuffer);
blockBuffer = "";
}
};
2026-01-07 11:08:11 +01:00
const emitReasoningStream = (text: string) => {
if (!streamReasoning || !params.onReasoningStream) return;
const formatted = formatReasoningMessage(text);
2026-01-07 11:08:11 +01:00
if (!formatted) return;
if (formatted === lastStreamedReasoning) return;
lastStreamedReasoning = formatted;
void params.onReasoningStream({
text: formatted,
});
};
2025-12-26 10:16:50 +01:00
const resetForCompactionRetry = () => {
assistantTexts.length = 0;
toolMetas.length = 0;
toolMetaById.clear();
toolSummaryById.clear();
messagingToolSentTexts.length = 0;
messagingToolSentTextsNormalized.length = 0;
2026-01-08 08:49:16 +01:00
messagingToolSentTargets.length = 0;
pendingMessagingTexts.clear();
2026-01-08 08:49:16 +01:00
pendingMessagingTargets.clear();
resetAssistantMessageState(0);
2025-12-26 10:16:50 +01:00
};
2025-12-22 20:45:22 +00:00
const unsubscribe = params.session.subscribe(
(evt: AgentEvent | { type: string; [k: string]: unknown }) => {
if (evt.type === "message_start") {
const msg = (evt as AgentEvent & { message: AgentMessage }).message;
if (msg?.role === "assistant") {
// KNOWN: Resetting at `text_end` is unsafe (late/duplicate end events).
// ASSUME: `message_start` is the only reliable boundary for “new assistant message begins”.
// Start-of-message is a safer reset point than message_end: some providers
// may deliver late text_end updates after message_end, which would
// otherwise re-trigger block replies.
resetAssistantMessageState(assistantTexts.length);
}
}
2025-12-22 20:45:22 +00:00
if (evt.type === "tool_execution_start") {
// Flush pending block replies to preserve message boundaries before tool execution.
flushBlockReplyBuffer();
if (params.onBlockReplyFlush) {
void params.onBlockReplyFlush();
}
2025-12-22 20:45:22 +00:00
const toolName = String(
(evt as AgentEvent & { toolName: string }).toolName,
);
const toolCallId = String(
(evt as AgentEvent & { toolCallId: string }).toolCallId,
);
const args = (evt as AgentEvent & { args: unknown }).args;
2026-01-10 18:12:32 +00:00
if (toolName === "read") {
const record =
args && typeof args === "object"
? (args as Record<string, unknown>)
: {};
const filePath =
typeof record.path === "string" ? record.path.trim() : "";
if (!filePath) {
const argsPreview =
typeof args === "string" ? args.slice(0, 200) : undefined;
log.warn(
`read tool called without path: toolCallId=${toolCallId} argsType=${typeof args}${argsPreview ? ` argsPreview=${argsPreview}` : ""}`,
);
}
}
2025-12-22 20:45:22 +00:00
const meta = inferToolMetaFromArgs(toolName, args);
toolMetaById.set(toolCallId, meta);
log.debug(
`embedded run tool start: runId=${params.runId} tool=${toolName} toolCallId=${toolCallId}`,
);
2025-12-22 20:45:22 +00:00
const shouldEmitToolEvents = shouldEmitToolResult();
emitAgentEvent({
runId: params.runId,
stream: "tool",
data: {
phase: "start",
name: toolName,
toolCallId,
args: args as Record<string, unknown>,
},
});
2025-12-22 20:45:22 +00:00
params.onAgentEvent?.({
stream: "tool",
data: { phase: "start", name: toolName, toolCallId },
});
if (
params.onToolResult &&
shouldEmitToolEvents &&
!toolSummaryById.has(toolCallId)
) {
toolSummaryById.add(toolCallId);
emitToolSummary(toolName, meta);
}
// Track messaging tool sends (pending until confirmed in tool_execution_end)
if (isMessagingTool(toolName)) {
const argsRecord =
args && typeof args === "object"
? (args as Record<string, unknown>)
: {};
const action =
typeof argsRecord.action === "string"
? argsRecord.action.trim()
: "";
Move provider to a plugin-architecture (#661) * refactor: introduce provider plugin registry * refactor: move provider CLI to plugins * docs: add provider plugin implementation notes * refactor: shift provider runtime logic into plugins * refactor: add plugin defaults and summaries * docs: update provider plugin notes * feat(commands): add /commands slash list * Auto-reply: tidy help message * Auto-reply: fix status command lint * Tests: align google shared expectations * Auto-reply: tidy help message * Auto-reply: fix status command lint * refactor: move provider routing into plugins * test: align agent routing expectations * docs: update provider plugin notes * refactor: route replies via provider plugins * docs: note route-reply plugin hooks * refactor: extend provider plugin contract * refactor: derive provider status from plugins * refactor: unify gateway provider control * refactor: use plugin metadata in auto-reply * fix: parenthesize cron target selection * refactor: derive gateway methods from plugins * refactor: generalize provider logout * refactor: route provider logout through plugins * refactor: move WhatsApp web login methods into plugin * refactor: generalize provider log prefixes * refactor: centralize default chat provider * refactor: derive provider lists from registry * refactor: move provider reload noops into plugins * refactor: resolve web login provider via alias * refactor: derive CLI provider options from plugins * refactor: derive prompt provider list from plugins * style: apply biome lint fixes * fix: resolve provider routing edge cases * docs: update provider plugin refactor notes * fix(gateway): harden agent provider routing * refactor: move provider routing into plugins * refactor: move provider CLI to plugins * refactor: derive provider lists from registry * fix: restore slash command parsing * refactor: align provider ids for schema * refactor: unify outbound target resolution * fix: keep outbound labels stable * feat: add msteams to cron surfaces * fix: clean up lint build issues * refactor: localize chat provider alias normalization * refactor: drive gateway provider lists from plugins * docs: update provider plugin notes * style: format message-provider * fix: avoid provider registry init cycles * style: sort message-provider imports * fix: relax provider alias map typing * refactor: move provider routing into plugins * refactor: add plugin pairing/config adapters * refactor: route pairing and provider removal via plugins * refactor: align auto-reply provider typing * test: stabilize telegram media mocks * docs: update provider plugin refactor notes * refactor: pluginize outbound targets * refactor: pluginize provider selection * refactor: generalize text chunk limits * docs: update provider plugin notes * refactor: generalize group session/config * fix: normalize provider id for room detection * fix: avoid provider init in system prompt * style: formatting cleanup * refactor: normalize agent delivery targets * test: update outbound delivery labels * chore: fix lint regressions * refactor: extend provider plugin adapters * refactor: move elevated/block streaming defaults to plugins * refactor: defer outbound send deps to plugins * docs: note plugin-driven streaming/elevated defaults * refactor: centralize webchat provider constant * refactor: add provider setup adapters * refactor: delegate provider add config to plugins * docs: document plugin-driven provider add * refactor: add plugin state/binding metadata * refactor: build agent provider status from plugins * docs: note plugin-driven agent bindings * refactor: centralize internal provider constant usage * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize default chat provider * refactor: centralize WhatsApp target normalization * refactor: move provider routing into plugins * refactor: normalize agent delivery targets * chore: fix lint regressions * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * feat: expand provider plugin adapters * refactor: route auto-reply via provider plugins * fix: align WhatsApp target normalization * fix: normalize WhatsApp targets for groups and E.164 (#631) (thanks @imfing) * refactor: centralize WhatsApp target normalization * feat: add /config chat config updates * docs: add /config get alias * feat(commands): add /commands slash list * refactor: centralize default chat provider * style: apply biome lint fixes * chore: fix lint regressions * fix: clean up whatsapp allowlist typing * style: format config command helpers * refactor: pluginize tool threading context * refactor: normalize session announce targets * docs: note new plugin threading and announce hooks * refactor: pluginize message actions * docs: update provider plugin actions notes * fix: align provider action adapters * refactor: centralize webchat checks * style: format message provider helpers * refactor: move provider onboarding into adapters * docs: note onboarding provider adapters * feat: add msteams onboarding adapter * style: organize onboarding imports * fix: normalize msteams allowFrom types * feat: add plugin text chunk limits * refactor: use plugin chunk limit fallbacks * feat: add provider mention stripping hooks * style: organize provider plugin type imports * refactor: generalize health snapshots * refactor: update macOS health snapshot handling * docs: refresh health snapshot notes * style: format health snapshot updates * refactor: drive security warnings via plugins * docs: note provider security adapter * style: format provider security adapters * refactor: centralize provider account defaults * refactor: type gateway client identity constants * chore: regen gateway protocol swift * fix: degrade health on failed provider probe * refactor: centralize pairing approve hint * docs: add plugin CLI command references * refactor: route auth and tool sends through plugins * docs: expand provider plugin hooks * refactor: document provider docking touchpoints * refactor: normalize internal provider defaults * refactor: streamline outbound delivery wiring * refactor: make provider onboarding plugin-owned * refactor: support provider-owned agent tools * refactor: move telegram draft chunking into telegram module * refactor: infer provider tool sends via extractToolSend * fix: repair plugin onboarding imports * refactor: de-dup outbound target normalization * style: tidy plugin and agent imports * refactor: data-drive provider selection line * fix: satisfy lint after provider plugin rebase * test: deflake gateway-cli coverage * style: format gateway-cli coverage test * refactor(provider-plugins): simplify provider ids * test(pairing-cli): avoid provider-specific ternary * style(macos): swiftformat HealthStore * refactor(sandbox): derive provider tool denylist * fix(sandbox): avoid plugin init in defaults * refactor(provider-plugins): centralize provider aliases * style(test): satisfy biome * refactor(protocol): v3 providers.status maps * refactor(ui): adapt to protocol v3 * refactor(macos): adapt to protocol v3 * test: update providers.status v3 fixtures * refactor(gateway): map provider runtime snapshot * test(gateway): update reload runtime snapshot * refactor(whatsapp): normalize heartbeat provider id * docs(refactor): update provider plugin notes * style: satisfy biome after rebase * fix: describe sandboxed elevated in prompt * feat(gateway): add agent image attachments + live probe * refactor: derive CLI provider options from plugins * fix(gateway): harden agent provider routing * fix(gateway): harden agent provider routing * refactor: align provider ids for schema * fix(protocol): keep agent provider string * fix(gateway): harden agent provider routing * fix(protocol): keep agent provider string * refactor: normalize agent delivery targets * refactor: support provider-owned agent tools * refactor(config): provider-keyed elevated allowFrom * style: satisfy biome * fix(gateway): appease provider narrowing * style: satisfy biome * refactor(reply): move group intro hints into plugin * fix(reply): avoid plugin registry init cycle * refactor(providers): add lightweight provider dock * refactor(gateway): use typed client id in connect * refactor(providers): document docks and avoid init cycles * refactor(providers): make media limit helper generic * fix(providers): break plugin registry import cycles * style: satisfy biome * refactor(status-all): build providers table from plugins * refactor(gateway): delegate web login to provider plugin * refactor(provider): drop web alias * refactor(provider): lazy-load monitors * style: satisfy lint/format * style: format status-all providers table * style: swiftformat gateway discovery model * test: make reload plan plugin-driven * fix: avoid token stringification in status-all * refactor: make provider IDs explicit in status * feat: warn on signal/imessage provider runtime errors * test: cover gateway provider runtime warnings in status * fix: add runtime kind to provider status issues * test: cover health degradation on probe failure * fix: keep routeReply lightweight * style: organize routeReply imports * refactor(web): extract auth-store helpers * refactor(whatsapp): lazy login imports * refactor(outbound): route replies via plugin outbound * docs: update provider plugin notes * style: format provider status issues * fix: make sandbox scope warning wrap-safe * refactor: load outbound adapters from provider plugins * docs: update provider plugin outbound notes * style(macos): fix swiftformat lint * docs: changelog for provider plugins * fix(macos): satisfy swiftformat * fix(macos): open settings via menu action * style: format after rebase * fix(macos): open Settings via menu action --------- Co-authored-by: LK <luke@kyohere.com> Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com> Co-authored-by: Xin <xin@imfing.com>
2026-01-11 11:45:25 +00:00
const isMessagingSend = isMessagingToolSendAction(
toolName,
argsRecord,
);
2026-01-10 13:38:28 +01:00
if (isMessagingSend) {
2026-01-08 08:49:16 +01:00
const sendTarget = extractMessagingToolSend(toolName, argsRecord);
if (sendTarget) {
pendingMessagingTargets.set(toolCallId, sendTarget);
}
// Field names vary by tool: Discord/Slack use "content", sessions_send uses "message"
const text =
(argsRecord.content as string) ?? (argsRecord.message as string);
if (text && typeof text === "string") {
pendingMessagingTexts.set(toolCallId, text);
log.debug(
`Tracking pending messaging text: tool=${toolName} action=${action} len=${text.length}`,
);
}
}
}
2025-12-22 20:45:22 +00:00
}
2026-01-03 06:16:49 +01:00
if (evt.type === "tool_execution_update") {
const toolName = String(
(evt as AgentEvent & { toolName: string }).toolName,
);
const toolCallId = String(
(evt as AgentEvent & { toolCallId: string }).toolCallId,
);
const partial = (evt as AgentEvent & { partialResult?: unknown })
.partialResult;
const sanitized = sanitizeToolResult(partial);
emitAgentEvent({
runId: params.runId,
stream: "tool",
data: {
phase: "update",
name: toolName,
toolCallId,
partialResult: sanitized,
},
});
2026-01-03 06:16:49 +01:00
params.onAgentEvent?.({
stream: "tool",
data: {
phase: "update",
name: toolName,
toolCallId,
},
});
}
2025-12-22 20:45:22 +00:00
if (evt.type === "tool_execution_end") {
const toolName = String(
(evt as AgentEvent & { toolName: string }).toolName,
);
const toolCallId = String(
(evt as AgentEvent & { toolCallId: string }).toolCallId,
);
const isError = Boolean(
(evt as AgentEvent & { isError: boolean }).isError,
);
2026-01-03 06:16:49 +01:00
const result = (evt as AgentEvent & { result?: unknown }).result;
2026-01-11 10:41:44 +00:00
const isToolError = isError || isToolResultError(result);
2026-01-03 06:16:49 +01:00
const sanitizedResult = sanitizeToolResult(result);
2025-12-22 20:45:22 +00:00
const meta = toolMetaById.get(toolCallId);
toolMetas.push({ toolName, meta });
toolMetaById.delete(toolCallId);
toolSummaryById.delete(toolCallId);
2025-12-22 20:45:22 +00:00
// Commit messaging tool text on success, discard on error
const pendingText = pendingMessagingTexts.get(toolCallId);
2026-01-08 08:49:16 +01:00
const pendingTarget = pendingMessagingTargets.get(toolCallId);
if (pendingText) {
pendingMessagingTexts.delete(toolCallId);
2026-01-11 10:41:44 +00:00
if (!isToolError) {
messagingToolSentTexts.push(pendingText);
messagingToolSentTextsNormalized.push(
normalizeTextForComparison(pendingText),
);
log.debug(
`Committed messaging text: tool=${toolName} len=${pendingText.length}`,
);
trimMessagingToolSent();
}
}
2026-01-08 08:49:16 +01:00
if (pendingTarget) {
pendingMessagingTargets.delete(toolCallId);
2026-01-11 10:41:44 +00:00
if (!isToolError) {
2026-01-08 08:49:16 +01:00
messagingToolSentTargets.push(pendingTarget);
trimMessagingToolSent();
2026-01-08 08:49:16 +01:00
}
}
emitAgentEvent({
runId: params.runId,
stream: "tool",
data: {
phase: "result",
name: toolName,
toolCallId,
meta,
2026-01-11 10:41:44 +00:00
isError: isToolError,
result: sanitizedResult,
},
});
2025-12-22 20:45:22 +00:00
params.onAgentEvent?.({
stream: "tool",
data: {
phase: "result",
name: toolName,
toolCallId,
meta,
2026-01-11 10:41:44 +00:00
isError: isToolError,
2025-12-22 20:45:22 +00:00
},
});
}
if (evt.type === "message_update") {
2026-01-02 23:37:08 +01:00
const msg = (evt as AgentEvent & { message: AgentMessage }).message;
2025-12-22 20:45:22 +00:00
if (msg?.role === "assistant") {
const assistantEvent = (
evt as AgentEvent & { assistantMessageEvent?: unknown }
).assistantMessageEvent;
const assistantRecord =
assistantEvent && typeof assistantEvent === "object"
? (assistantEvent as Record<string, unknown>)
: undefined;
const evtType =
typeof assistantRecord?.type === "string"
? assistantRecord.type
: "";
if (
evtType === "text_delta" ||
evtType === "text_start" ||
evtType === "text_end"
) {
2026-01-03 18:44:07 +01:00
const delta =
2025-12-22 20:45:22 +00:00
typeof assistantRecord?.delta === "string"
? assistantRecord.delta
2026-01-03 18:44:07 +01:00
: "";
const content =
typeof assistantRecord?.content === "string"
? assistantRecord.content
: "";
2026-01-09 03:45:14 +00:00
appendRawStream({
ts: Date.now(),
event: "assistant_text_stream",
runId: params.runId,
sessionId: (params.session as { id?: string }).id,
evtType,
delta,
content,
});
2026-01-03 18:44:07 +01:00
let chunk = "";
if (evtType === "text_delta") {
chunk = delta;
} else if (evtType === "text_start" || evtType === "text_end") {
if (delta) {
chunk = delta;
} else if (content) {
// KNOWN: Some providers resend full content on `text_end`.
// We only append a suffix (or nothing) to keep output monotonic.
// Providers may resend full content on text_end; append only the suffix.
2026-01-03 18:44:07 +01:00
if (content.startsWith(deltaBuffer)) {
chunk = content.slice(deltaBuffer.length);
} else if (deltaBuffer.startsWith(content)) {
chunk = "";
} else if (!deltaBuffer.includes(content)) {
chunk = content;
}
}
}
2025-12-22 20:45:22 +00:00
if (chunk) {
deltaBuffer += chunk;
if (blockChunker) {
blockChunker.append(chunk);
} else {
blockBuffer += chunk;
}
}
2026-01-07 11:08:11 +01:00
if (streamReasoning) {
// Handle partial <think> tags: stream whatever reasoning is visible so far.
emitReasoningStream(extractThinkingFromTaggedStream(deltaBuffer));
2026-01-07 11:08:11 +01:00
}
const next = stripBlockTags(deltaBuffer, {
thinking: false,
final: false,
}).trim();
if (next && next !== lastStreamedAssistant) {
lastStreamedAssistant = next;
const { text: cleanedText, mediaUrls } =
parseReplyDirectives(next);
emitAgentEvent({
runId: params.runId,
stream: "assistant",
data: {
text: cleanedText,
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
},
});
params.onAgentEvent?.({
stream: "assistant",
data: {
text: cleanedText,
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
},
});
if (params.onPartialReply && shouldEmitPartialReplies) {
void params.onPartialReply({
text: cleanedText,
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
});
}
}
2026-01-05 17:21:44 +00:00
if (
params.onBlockReply &&
blockChunking &&
blockReplyBreak === "text_end"
) {
blockChunker?.drain({ force: false, emit: emitBlockChunk });
2026-01-03 16:45:53 +01:00
}
if (evtType === "text_end" && blockReplyBreak === "text_end") {
if (blockChunker?.hasBuffered()) {
blockChunker.drain({ force: true, emit: emitBlockChunk });
blockChunker.reset();
2026-01-05 17:15:17 +00:00
} else if (blockBuffer.length > 0) {
emitBlockChunk(blockBuffer);
blockBuffer = "";
2025-12-22 20:45:22 +00:00
}
}
}
}
}
if (evt.type === "message_end") {
2026-01-02 23:37:08 +01:00
const msg = (evt as AgentEvent & { message: AgentMessage }).message;
2025-12-22 20:45:22 +00:00
if (msg?.role === "assistant") {
const assistantMessage = msg as AssistantMessage;
promoteThinkingTagsToBlocks(assistantMessage);
const rawText = extractAssistantText(assistantMessage);
2026-01-09 03:45:14 +00:00
appendRawStream({
ts: Date.now(),
event: "assistant_message_end",
runId: params.runId,
sessionId: (params.session as { id?: string }).id,
rawText,
rawThinking: extractAssistantThinking(assistantMessage),
});
const text = stripBlockTags(rawText, {
thinking: false,
final: false,
});
2026-01-07 11:08:11 +01:00
const rawThinking =
includeReasoning || streamReasoning
? extractAssistantThinking(assistantMessage) ||
extractThinkingFromTaggedText(rawText)
2026-01-07 11:08:11 +01:00
: "";
const formattedReasoning = rawThinking
? formatReasoningMessage(rawThinking)
: "";
const addedDuringMessage =
assistantTexts.length > assistantTextBaseline;
const chunkerHasBuffered = blockChunker?.hasBuffered() ?? false;
finalizeAssistantTexts({
text,
addedDuringMessage,
chunkerHasBuffered,
});
const onBlockReply = params.onBlockReply;
const shouldEmitReasoning = Boolean(
includeReasoning &&
formattedReasoning &&
onBlockReply &&
formattedReasoning !== lastReasoningSent,
);
const shouldEmitReasoningBeforeAnswer =
shouldEmitReasoning &&
blockReplyBreak === "message_end" &&
!addedDuringMessage;
const maybeEmitReasoning = () => {
if (!shouldEmitReasoning || !formattedReasoning) return;
lastReasoningSent = formattedReasoning;
void onBlockReply?.({ text: formattedReasoning });
};
if (shouldEmitReasoningBeforeAnswer) maybeEmitReasoning();
if (
(blockReplyBreak === "message_end" ||
(blockChunker
? blockChunker.hasBuffered()
: blockBuffer.length > 0)) &&
text &&
onBlockReply
) {
if (blockChunker?.hasBuffered()) {
blockChunker.drain({ force: true, emit: emitBlockChunk });
blockChunker.reset();
2026-01-03 16:45:53 +01:00
} else if (text !== lastBlockReplyText) {
// Check for duplicates before emitting (same logic as emitBlockChunk)
const normalizedText = normalizeTextForComparison(text);
if (
isMessagingToolDuplicateNormalized(
normalizedText,
messagingToolSentTextsNormalized,
)
) {
log.debug(
`Skipping message_end block reply - already sent via messaging tool: ${text.slice(0, 50)}...`,
);
} else {
lastBlockReplyText = text;
const {
text: cleanedText,
mediaUrls,
audioAsVoice,
} = parseReplyDirectives(text);
// Emit if there's content OR audioAsVoice flag (to propagate the flag)
if (
cleanedText ||
(mediaUrls && mediaUrls.length > 0) ||
audioAsVoice
) {
void onBlockReply({
text: cleanedText,
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
audioAsVoice,
});
}
2026-01-03 16:45:53 +01:00
}
2026-01-03 00:28:33 +01:00
}
}
if (!shouldEmitReasoningBeforeAnswer) maybeEmitReasoning();
2026-01-07 11:08:11 +01:00
if (streamReasoning && rawThinking) {
emitReasoningStream(rawThinking);
}
2025-12-22 20:45:22 +00:00
deltaBuffer = "";
2026-01-03 16:45:53 +01:00
blockBuffer = "";
blockChunker?.reset();
blockState.thinking = false;
blockState.final = false;
lastStreamedAssistant = undefined;
2025-12-22 20:45:22 +00:00
}
}
if (evt.type === "tool_execution_end") {
const toolName = String(
(evt as AgentEvent & { toolName: string }).toolName,
);
const toolCallId = String(
(evt as AgentEvent & { toolCallId: string }).toolCallId,
);
log.debug(
`embedded run tool end: runId=${params.runId} tool=${toolName} toolCallId=${toolCallId}`,
);
}
if (evt.type === "agent_start") {
log.debug(`embedded run agent start: runId=${params.runId}`);
2026-01-05 05:55:02 +01:00
emitAgentEvent({
runId: params.runId,
stream: "lifecycle",
data: {
phase: "start",
startedAt: Date.now(),
},
});
params.onAgentEvent?.({
stream: "lifecycle",
data: { phase: "start" },
});
}
2025-12-26 10:16:50 +01:00
if (evt.type === "auto_compaction_start") {
compactionInFlight = true;
ensureCompactionPromise();
log.debug(`embedded run compaction start: runId=${params.runId}`);
params.onAgentEvent?.({
stream: "compaction",
data: { phase: "start" },
});
2025-12-26 10:16:50 +01:00
}
if (evt.type === "auto_compaction_end") {
compactionInFlight = false;
const willRetry = Boolean((evt as { willRetry?: unknown }).willRetry);
2025-12-26 10:16:50 +01:00
if (willRetry) {
noteCompactionRetry();
resetForCompactionRetry();
log.debug(`embedded run compaction retry: runId=${params.runId}`);
2025-12-26 10:16:50 +01:00
} else {
maybeResolveCompactionWait();
}
params.onAgentEvent?.({
stream: "compaction",
data: { phase: "end", willRetry },
});
2025-12-26 10:16:50 +01:00
}
2025-12-22 20:45:22 +00:00
if (evt.type === "agent_end") {
log.debug(`embedded run agent end: runId=${params.runId}`);
2026-01-05 05:55:02 +01:00
emitAgentEvent({
runId: params.runId,
stream: "lifecycle",
data: {
phase: "end",
endedAt: Date.now(),
},
});
params.onAgentEvent?.({
stream: "lifecycle",
data: { phase: "end" },
});
if (params.onBlockReply) {
if (blockChunker?.hasBuffered()) {
blockChunker.drain({ force: true, emit: emitBlockChunk });
blockChunker.reset();
} else if (blockBuffer.length > 0) {
emitBlockChunk(blockBuffer);
blockBuffer = "";
}
}
blockState.thinking = false;
blockState.final = false;
2025-12-26 10:16:50 +01:00
if (pendingCompactionRetry > 0) {
resolveCompactionRetry();
} else {
maybeResolveCompactionWait();
}
2025-12-22 20:45:22 +00:00
}
},
);
return {
assistantTexts,
toolMetas,
unsubscribe,
isCompacting: () => compactionInFlight || pendingCompactionRetry > 0,
getMessagingToolSentTexts: () => messagingToolSentTexts.slice(),
2026-01-08 08:49:16 +01:00
getMessagingToolSentTargets: () => messagingToolSentTargets.slice(),
// Returns true if any messaging tool successfully sent a message.
// Used to suppress agent's confirmation text (e.g., "Respondi no Telegram!")
// which is generated AFTER the tool sends the actual answer.
didSendViaMessagingTool: () => messagingToolSentTexts.length > 0,
2025-12-26 10:16:50 +01:00
waitForCompactionRetry: () => {
if (compactionInFlight || pendingCompactionRetry > 0) {
ensureCompactionPromise();
return compactionRetryPromise ?? Promise.resolve();
}
return new Promise<void>((resolve) => {
2025-12-26 10:16:50 +01:00
queueMicrotask(() => {
if (compactionInFlight || pendingCompactionRetry > 0) {
ensureCompactionPromise();
void (compactionRetryPromise ?? Promise.resolve()).then(resolve);
} else {
resolve();
}
});
});
},
2025-12-22 20:45:22 +00:00
};
}