Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/apis/sms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { context, SpanStatusCode, trace } from '@opentelemetry/api';
import type { SmsReply } from '../io/sms';
import { getTracer, recordException } from '../router/router';
import { safeStringify } from '../server/util';
import type { AgentContext, AgentRequest, SMSService } from '../types';
import { POST } from './api';

export default class SmsApi implements SMSService {
async send(
_req: AgentRequest,
ctx: AgentContext,
to: string[],
message: SmsReply,
from?: string
): Promise<void> {
const timeout = 15_000;
const tracer = getTracer();
const currentContext = context.active();

const span = tracer.startSpan('agentuity.sms.send', {}, currentContext);

try {
const spanContext = trace.setSpan(currentContext, span);

return await context.with(spanContext, async () => {
span.setAttribute('@agentuity/agentId', ctx.agent.id);
const resp = await POST(
'/sms/send',
safeStringify({
agentId: ctx.agent.id,
from: from,
to: to,
message: message.text,
}),
{
'Content-Type': 'application/json',
Accept: 'application/json',
},
timeout
);
if (resp.status === 200) {
span.setStatus({ code: SpanStatusCode.OK });
return;
}
throw new Error(
`error sending sms: ${resp.response.statusText} (${resp.response.status}): ${resp.json}`
);
});
} catch (ex) {
recordException(span, ex);
throw ex;
} finally {
span.end();
}
}

/**
* @deprecated Use reply on data.email.sms()
*/
async sendReply(
_agentId: string,
_phoneNumber: string,
_authToken: string,
_messageId: string
): Promise<void> {
throw new Error('reply not supported in context');
}
}
61 changes: 59 additions & 2 deletions src/io/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type TwilioResponse = {
};

/**
* A reply to an email
* A reply to an SMS
*/
export interface SmsReply {
/**
Expand Down Expand Up @@ -54,7 +54,63 @@ export class Sms {
return this._message.Body;
}

async send(
req: AgentRequest,
ctx: AgentContext,
to: string[],
message: SmsReply,
from?: string
) {
const timeout = 15_000;
const tracer = getTracer();
const currentContext = context.active();

const authToken = req.metadata?.['twilio-auth-token'] as string;
if (!authToken) {
throw new Error(
'twilio authorization token is required but not found in metadata'
);
}

const span = tracer.startSpan('agentuity.twilio.send', {}, currentContext);

try {
const spanContext = trace.setSpan(currentContext, span);

return await context.with(spanContext, async () => {
span.setAttribute('@agentuity/agentId', ctx.agent.id);
const resp = await POST(
'/sms/send',
safeStringify({
agentId: ctx.agent.id,
from: from,
to: to,
message: message.text,
}),
{
'Content-Type': 'application/json',
},
timeout,
authToken
);
if (resp.status === 200) {
span.setStatus({ code: SpanStatusCode.OK });
return;
}
throw new Error(
`error sending sms: ${resp.response.statusText} (${resp.response.status})`
);
});
} catch (ex) {
recordException(span, ex);
throw ex;
} finally {
span.end();
}
}

async sendReply(req: AgentRequest, ctx: AgentContext, reply: string) {
const timeout = 15_000;
const tracer = getTracer();
const currentContext = context.active();

Expand Down Expand Up @@ -87,7 +143,7 @@ export class Sms {
'Content-Type': 'application/json',
'X-Agentuity-Message-Id': this.messageId,
},
undefined,
timeout,
authToken
);
if (resp.status === 200) {
Expand Down Expand Up @@ -120,3 +176,4 @@ export async function parseSms(data: Buffer): Promise<Sms> {
);
}
}

3 changes: 3 additions & 0 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import KeyValueAPI from '../apis/keyvalue';
import ObjectStoreAPI from '../apis/objectstore';
import PatchPortal from '../apis/patchportal';
import PromptAPI from '../apis/prompt/index.js';
import SmsAPI from '../apis/sms';
import StreamAPIImpl from '../apis/stream';
import VectorAPI from '../apis/vector';
import type { Logger } from '../logger';
Expand Down Expand Up @@ -182,6 +183,7 @@ const email = new EmailAPI();
const discord = new DiscordAPI();
const objectstore = new ObjectStoreAPI();
const prompts = new PromptAPI();
const sms = new SmsAPI();

// PatchPortal will be initialized lazily since it's async
let patchportal: PatchPortal | null = null;
Expand Down Expand Up @@ -222,6 +224,7 @@ export async function createServerContext(
discord,
objectstore,
patchportal,
sms,
sdkVersion: req.sdkVersion,
agents: req.agents,
scope: 'local',
Expand Down
18 changes: 17 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,17 @@ export interface SMSService {
/**
* send an SMS to a phone number
*/
send(
req: AgentRequest,
context: AgentContext,
to: string[],
message: import('./io/sms').SmsReply,
from?: string
): Promise<void>;

/**
* send an SMS reply to an incoming SMS message
*/
sendReply(
agentId: string,
phoneNumber: string,
Expand Down Expand Up @@ -1059,6 +1070,11 @@ export interface AgentContext {
*/
slack: SlackService;

/**
* the sms service
*/
sms: SMSService;

/**
* EXPERIMENTAL: prompts API for accessing and compiling prompts
*/
Expand Down Expand Up @@ -1351,4 +1367,4 @@ export interface DataPayload {
* the metadata
*/
metadata?: JsonObject;
}
}
Loading