refactor: unify outbound result envelopes
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
registerAgentRunContext,
|
||||
} from "../infra/agent-events.js";
|
||||
import { deliverOutboundPayloads } from "../infra/outbound/deliver.js";
|
||||
import { buildOutboundResultEnvelope } from "../infra/outbound/envelope.js";
|
||||
import {
|
||||
formatOutboundPayloadLog,
|
||||
type NormalizedOutboundPayload,
|
||||
@@ -548,7 +549,10 @@ export async function agentCommand(
|
||||
if (opts.json) {
|
||||
runtime.log(
|
||||
JSON.stringify(
|
||||
{ payloads: normalizedPayloads, meta: result.meta },
|
||||
buildOutboundResultEnvelope({
|
||||
payloads: normalizedPayloads,
|
||||
meta: result.meta,
|
||||
}),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { success } from "../globals.js";
|
||||
import { buildOutboundResultEnvelope } from "../infra/outbound/envelope.js";
|
||||
import {
|
||||
buildOutboundDeliveryJson,
|
||||
formatGatewaySummary,
|
||||
@@ -89,12 +90,14 @@ export async function pollCommand(
|
||||
runtime.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...buildOutboundDeliveryJson({
|
||||
provider,
|
||||
via: "gateway",
|
||||
to: opts.to,
|
||||
result,
|
||||
mediaUrl: null,
|
||||
...buildOutboundResultEnvelope({
|
||||
delivery: buildOutboundDeliveryJson({
|
||||
provider,
|
||||
via: "gateway",
|
||||
to: opts.to,
|
||||
result,
|
||||
mediaUrl: null,
|
||||
}),
|
||||
}),
|
||||
question: normalized.question,
|
||||
options: normalized.options,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { loadConfig } from "../config/config.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { success } from "../globals.js";
|
||||
import { deliverOutboundPayloads } from "../infra/outbound/deliver.js";
|
||||
import { buildOutboundResultEnvelope } from "../infra/outbound/envelope.js";
|
||||
import {
|
||||
buildOutboundDeliveryJson,
|
||||
formatGatewaySummary,
|
||||
@@ -114,12 +115,14 @@ export async function sendCommand(
|
||||
if (opts.json) {
|
||||
runtime.log(
|
||||
JSON.stringify(
|
||||
buildOutboundDeliveryJson({
|
||||
provider,
|
||||
via: "gateway",
|
||||
to: opts.to,
|
||||
result,
|
||||
mediaUrl: opts.media ?? null,
|
||||
buildOutboundResultEnvelope({
|
||||
delivery: buildOutboundDeliveryJson({
|
||||
provider,
|
||||
via: "gateway",
|
||||
to: opts.to,
|
||||
result,
|
||||
mediaUrl: opts.media ?? null,
|
||||
}),
|
||||
}),
|
||||
null,
|
||||
2,
|
||||
|
||||
@@ -40,6 +40,18 @@ export type OutboundDeliveryResult =
|
||||
|
||||
type Chunker = (text: string, limit: number) => string[];
|
||||
|
||||
const providerCaps: Record<
|
||||
Exclude<OutboundProvider, "none">,
|
||||
{ chunker: Chunker | null }
|
||||
> = {
|
||||
whatsapp: { chunker: chunkText },
|
||||
telegram: { chunker: chunkMarkdownText },
|
||||
discord: { chunker: null },
|
||||
slack: { chunker: null },
|
||||
signal: { chunker: chunkText },
|
||||
imessage: { chunker: chunkText },
|
||||
};
|
||||
|
||||
type ProviderHandler = {
|
||||
chunker: Chunker | null;
|
||||
sendText: (text: string) => Promise<OutboundDeliveryResult>;
|
||||
@@ -82,7 +94,7 @@ function createProviderHandler(params: {
|
||||
|
||||
const handlers: Record<Exclude<OutboundProvider, "none">, ProviderHandler> = {
|
||||
whatsapp: {
|
||||
chunker: chunkText,
|
||||
chunker: providerCaps.whatsapp.chunker,
|
||||
sendText: async (text) => ({
|
||||
provider: "whatsapp",
|
||||
...(await deps.sendWhatsApp(to, text, { verbose: false })),
|
||||
@@ -96,7 +108,7 @@ function createProviderHandler(params: {
|
||||
}),
|
||||
},
|
||||
telegram: {
|
||||
chunker: chunkMarkdownText,
|
||||
chunker: providerCaps.telegram.chunker,
|
||||
sendText: async (text) => ({
|
||||
provider: "telegram",
|
||||
...(await deps.sendTelegram(to, text, {
|
||||
@@ -114,7 +126,7 @@ function createProviderHandler(params: {
|
||||
}),
|
||||
},
|
||||
discord: {
|
||||
chunker: null,
|
||||
chunker: providerCaps.discord.chunker,
|
||||
sendText: async (text) => ({
|
||||
provider: "discord",
|
||||
...(await deps.sendDiscord(to, text, { verbose: false })),
|
||||
@@ -128,7 +140,7 @@ function createProviderHandler(params: {
|
||||
}),
|
||||
},
|
||||
slack: {
|
||||
chunker: null,
|
||||
chunker: providerCaps.slack.chunker,
|
||||
sendText: async (text) => ({
|
||||
provider: "slack",
|
||||
...(await deps.sendSlack(to, text)),
|
||||
@@ -139,7 +151,7 @@ function createProviderHandler(params: {
|
||||
}),
|
||||
},
|
||||
signal: {
|
||||
chunker: chunkText,
|
||||
chunker: providerCaps.signal.chunker,
|
||||
sendText: async (text) => ({
|
||||
provider: "signal",
|
||||
...(await deps.sendSignal(to, text, { maxBytes: signalMaxBytes })),
|
||||
@@ -153,7 +165,7 @@ function createProviderHandler(params: {
|
||||
}),
|
||||
},
|
||||
imessage: {
|
||||
chunker: chunkText,
|
||||
chunker: providerCaps.imessage.chunker,
|
||||
sendText: async (text) => ({
|
||||
provider: "imessage",
|
||||
...(await deps.sendIMessage(to, text, { maxBytes: imessageMaxBytes })),
|
||||
|
||||
65
src/infra/outbound/envelope.test.ts
Normal file
65
src/infra/outbound/envelope.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildOutboundResultEnvelope } from "./envelope.js";
|
||||
import type { OutboundDeliveryJson } from "./format.js";
|
||||
|
||||
describe("buildOutboundResultEnvelope", () => {
|
||||
it("flattens delivery-only payloads by default", () => {
|
||||
const delivery: OutboundDeliveryJson = {
|
||||
provider: "whatsapp",
|
||||
via: "gateway",
|
||||
to: "+1",
|
||||
messageId: "m1",
|
||||
mediaUrl: null,
|
||||
};
|
||||
expect(buildOutboundResultEnvelope({ delivery })).toEqual(delivery);
|
||||
});
|
||||
|
||||
it("keeps payloads and meta in the envelope", () => {
|
||||
const envelope = buildOutboundResultEnvelope({
|
||||
payloads: [{ text: "hi", mediaUrl: null, mediaUrls: undefined }],
|
||||
meta: { foo: "bar" },
|
||||
});
|
||||
expect(envelope).toEqual({
|
||||
payloads: [{ text: "hi", mediaUrl: null, mediaUrls: undefined }],
|
||||
meta: { foo: "bar" },
|
||||
});
|
||||
});
|
||||
|
||||
it("includes delivery when payloads are present", () => {
|
||||
const delivery: OutboundDeliveryJson = {
|
||||
provider: "telegram",
|
||||
via: "direct",
|
||||
to: "123",
|
||||
messageId: "m2",
|
||||
mediaUrl: null,
|
||||
chatId: "c1",
|
||||
};
|
||||
const envelope = buildOutboundResultEnvelope({
|
||||
payloads: [],
|
||||
delivery,
|
||||
meta: { ok: true },
|
||||
});
|
||||
expect(envelope).toEqual({
|
||||
payloads: [],
|
||||
meta: { ok: true },
|
||||
delivery,
|
||||
});
|
||||
});
|
||||
|
||||
it("can keep delivery wrapped when requested", () => {
|
||||
const delivery: OutboundDeliveryJson = {
|
||||
provider: "discord",
|
||||
via: "gateway",
|
||||
to: "channel:C1",
|
||||
messageId: "m3",
|
||||
mediaUrl: null,
|
||||
channelId: "C1",
|
||||
};
|
||||
const envelope = buildOutboundResultEnvelope({
|
||||
delivery,
|
||||
flattenDelivery: false,
|
||||
});
|
||||
expect(envelope).toEqual({ delivery });
|
||||
});
|
||||
});
|
||||
52
src/infra/outbound/envelope.ts
Normal file
52
src/infra/outbound/envelope.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { OutboundDeliveryJson } from "./format.js";
|
||||
import {
|
||||
normalizeOutboundPayloadsForJson,
|
||||
type OutboundPayloadJson,
|
||||
} from "./payloads.js";
|
||||
|
||||
export type OutboundResultEnvelope = {
|
||||
payloads?: OutboundPayloadJson[];
|
||||
meta?: unknown;
|
||||
delivery?: OutboundDeliveryJson;
|
||||
};
|
||||
|
||||
type BuildEnvelopeParams = {
|
||||
payloads?: ReplyPayload[] | OutboundPayloadJson[];
|
||||
meta?: unknown;
|
||||
delivery?: OutboundDeliveryJson;
|
||||
flattenDelivery?: boolean;
|
||||
};
|
||||
|
||||
const isOutboundPayloadJson = (
|
||||
payload: ReplyPayload | OutboundPayloadJson,
|
||||
): payload is OutboundPayloadJson => "mediaUrl" in payload;
|
||||
|
||||
export function buildOutboundResultEnvelope(
|
||||
params: BuildEnvelopeParams,
|
||||
): OutboundResultEnvelope | OutboundDeliveryJson {
|
||||
const hasPayloads = params.payloads !== undefined;
|
||||
const payloads =
|
||||
params.payloads === undefined
|
||||
? undefined
|
||||
: params.payloads.length === 0
|
||||
? []
|
||||
: isOutboundPayloadJson(params.payloads[0])
|
||||
? (params.payloads as OutboundPayloadJson[])
|
||||
: normalizeOutboundPayloadsForJson(params.payloads as ReplyPayload[]);
|
||||
|
||||
if (
|
||||
params.flattenDelivery !== false &&
|
||||
params.delivery &&
|
||||
!params.meta &&
|
||||
!hasPayloads
|
||||
) {
|
||||
return params.delivery;
|
||||
}
|
||||
|
||||
return {
|
||||
...(hasPayloads ? { payloads } : {}),
|
||||
...(params.meta ? { meta: params.meta } : {}),
|
||||
...(params.delivery ? { delivery: params.delivery } : {}),
|
||||
};
|
||||
}
|
||||
@@ -174,17 +174,26 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
}
|
||||
|
||||
if (provider !== "whatsapp") {
|
||||
return { provider, to };
|
||||
const resolved = resolveOutboundTarget({ provider, to });
|
||||
return resolved.ok
|
||||
? { provider, to: resolved.to }
|
||||
: { provider: "none", reason: "no-target" };
|
||||
}
|
||||
|
||||
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
|
||||
if (rawAllow.includes("*")) return { provider, to };
|
||||
const resolved = resolveOutboundTarget({
|
||||
provider: "whatsapp",
|
||||
to,
|
||||
allowFrom: rawAllow,
|
||||
});
|
||||
if (!resolved.ok) {
|
||||
return { provider: "none", reason: "no-target" };
|
||||
}
|
||||
if (rawAllow.includes("*")) return { provider, to: resolved.to };
|
||||
const allowFrom = rawAllow
|
||||
.map((val) => normalizeE164(val))
|
||||
.filter((val) => val.length > 1);
|
||||
if (allowFrom.length === 0) return { provider, to };
|
||||
|
||||
const normalized = normalizeE164(to);
|
||||
if (allowFrom.includes(normalized)) return { provider, to: normalized };
|
||||
if (allowFrom.length === 0) return { provider, to: resolved.to };
|
||||
if (allowFrom.includes(resolved.to)) return { provider, to: resolved.to };
|
||||
return { provider, to: allowFrom[0], reason: "allowFrom-fallback" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user