Agnost AIAgnost AIBook a demo
Open +
← All articles

Photon Spectrum Observability: Monitor AI Agents Across Messaging Channels

Monitor Photon Spectrum agents with Agnost using a real spectrum-ts message handler. Reproduce a failed send and retry, inspect captured events, and preserve conversation identity.

Photon builds Spectrum, an open-source framework for agents that communicate through messaging channels. The TypeScript package is spectrum-ts. This guide is about Photon’s Spectrum, including applications that use its iMessage, Slack, Telegram, or WhatsApp providers.

A messaging assistant can generate the right reply and still fail to send it. It can also send twice after a redelivery, or group two unrelated people under the same identifier. Model traces alone do not explain those failures.

Spectrum-TS observability should connect incoming messages, reply generation, send results, and stable channel identity. Spectrum’s native telemetry and your application’s conversation records provide different evidence. In particular, the native telemetry path documented in Agnost’s integration does not include message transcript text; add an explicit content boundary when you need conversation analysis.

Agnost AI provides agent analytics, observability, and monitoring across application surfaces. Its Spectrum-TS integration supports native OpenTelemetry plus explicit conversation tracking. Spectrum handles messaging; your application determines how an agent generates and sends a reply.

Spectrum-TS monitoring boundaries: Inbound message → Reply generation → Send accepted → Delivery receipt → User response
Monitoring model for Spectrum-TS. Provider acceptance, delivery, and reading are different outcomes.

Run a Photon Spectrum observability example locally

This example uses the published [email protected] and [email protected] packages. We ran it with Node.js 24.19.0 on October 5, 2026. It processes a real Spectrum Message, invokes message.reply(), and captures the Agnost SDK’s outgoing HTTP records on localhost.

The channel is simulated. The first send deliberately rejects before acceptance; the second returns an outbound message. Reply generation is deterministic. No LLM, Photon Cloud account, hosted Agnost ingestion, or real recipient is involved. This demonstrates the SDK interaction boundary, not live iMessage delivery or measured model performance.

Download the complete runnable example, or save these files into one folder:

pnpm install --frozen-lockfile --ignore-scripts
pnpm demo

No credentials are required for this local command. It writes captured-run.json into the current directory. The deliberate failure is part of the demonstration; the process then makes one explicit second attempt.

Captured field First attempt Second attempt
inboundMessageId inbound-001 inbound-001
Conversation demo:local_demo:support-42 Same conversation
attemptId Unique attempt ID A different attempt ID
success false true
sendState send_error provider_returned_message
outboundMessageId Absent outbound-001
deliveryState unknown unknown

Inspect the actual captured SDK requests. There are two interaction events. The SDK also issued session registration requests; those are not additional user messages. One inbound request plus two processing attempts must not be reported as three customer requests.

Agnost local demo replay showing one Spectrum input, a simulated send rejection, and the subsequent reply
Local SDK replay. Captured input/output records loaded into Agnost’s demo UI. The failure bubble is diagnostic output, not a message delivered to the customer. This is not a hosted ingestion screenshot; the surrounding demo controls use fixtures. The near-zero latency reflects a local deterministic reply, not LLM performance.

Map actual Spectrum messages to Agnost

Use message.sender.id for the channel participant, space.id for the space, message.id for the incoming event, and message.content.text after narrowing the content type. Namespace identities by tenant and platform. A group space may contain multiple conversations; add the appropriate application or provider thread key if your product needs a narrower grouping.

The following is the handler executed in the local demonstration. It accepts Spectrum’s actual objects, rather than an invented normalized message interface:

import { randomUUID } from 'node:crypto';
import * as agnost from 'agnostai';

// Accepts real Spectrum Space and Message objects. Initialize Agnost at startup.
export async function observeMessage(space, message, { tenant, createReply, redact }) {
  if (message.direction !== 'inbound' || message.content.type !== 'text' || !message.sender) return;
  const key = (...parts) => parts.map(encodeURIComponent).join(':');
  const interaction = agnost.begin({
    userId: key(tenant, message.platform, message.sender.id),
    conversationId: key(tenant, message.platform, space.id),
    agentName: 'photon-spectrum-assistant',
    input: redact(message.content.text),
    properties: {
      framework: 'spectrum-ts', channel: message.platform,
      inboundMessageId: message.id, attemptId: randomUUID(),
      deliveryState: 'unknown',
    },
  });
  let phase = 'generation';
  try {
    const reply = await createReply(message.content.text);
    phase = 'send';
    const sent = await message.reply(reply);
    // A returned message is provider evidence, not a delivery/read receipt.
    interaction?.setProperty('sendState', sent ? 'provider_returned_message' : 'returned_without_message');
    if (sent) interaction?.setProperty('outboundMessageId', sent.id);
    interaction?.end(redact(reply), true);
    return sent;
  } catch (error) {
    interaction?.setProperty('failedPhase', phase);
    interaction?.setProperty('sendState', phase === 'send' ? 'send_error' : 'not_attempted');
    // A send error can be ambiguous. Do not automatically resend in production.
    interaction?.end(error instanceof Error ? error.name : 'UnknownError', false);
    throw error;
  }
}

Provide a redaction function appropriate to your data policy. The handler uses the original text for reply generation and records redacted copies. Configure Agnost once at application startup and flush with await agnost.shutdown() on exit.

A resolved message.reply() does not prove that someone received or read the message. The example records whether the provider returned an outbound message, retaining its ID when present. A return without a message is a separate state. A send error may also be ambiguous: a timeout can occur after the provider accepted the operation. Do not copy the demonstration’s second attempt into an automatic retry policy.

Connect the handler to an iMessage agent

For a Photon Cloud iMessage application, keep the same handler and replace the local provider with the official iMessage provider. Download the complete cloud loop, then configure PROJECT_ID, PROJECT_SECRET, AGNOST_ORG_ID, and your own APP_TENANT_ID in the server environment.

import { Spectrum } from 'spectrum-ts';
import { imessage } from 'spectrum-ts/providers/imessage';
import * as agnost from 'agnostai';
import { observeMessage } from './observe-message.mjs';

agnost.init(process.env.AGNOST_ORG_ID);
const app = await Spectrum({
  projectId: process.env.PROJECT_ID,
  projectSecret: process.env.PROJECT_SECRET,
  providers: [imessage.config()],
  telemetry: false,
});

// Define these in your app: createReply, redact, and a tenant identity.
try {
  for await (const [space, message] of app.messages) {
    await observeMessage(space, message, {
      tenant: process.env.APP_TENANT_ID,
      createReply,
      redact,
    });
  }
} finally {
  await app.stop();
  await agnost.shutdown();
}

This cloud wiring follows the published SDK, but was not executed against a Photon account for this article. The downloadable cloud loop includes shutdown handling and a deterministic echo reply; replace that reply with your agent and its minimal email-redaction example with your actual content policy before processing private messages. It sends real replies when run with a connected account. See Photon’s setup instructions for channel provisioning.

Start with explicit interaction tracking as shown. Enable native telemetry separately when you need transport spans, and inspect the resulting records before combining counts. Neither path automatically establishes business resolution.

Metrics that answer useful Spectrum-TS questions

Use these definitions to design your measurement plan. Calculate them from the required application or tracing evidence; they are not all precomputed dashboard metrics.

Metric Definition Interpretation
Duplicate reply rate Inbound messages producing unintended multiple replies / unique inbound messages Requires stable inbound IDs and a defined retry policy.
Send acceptance rate Accepted sends / attempted sends Does not establish delivery or reading.
Reply generation failures Failed generations / generation attempts Keep channel transport errors in a separate category.

Model the message lifecycle before counting success

For a channel assistant, at least four events can matter:

Stage What is known
Message received Your application received an input
Reply generated The agent produced content
Send accepted The channel API accepted the outgoing operation
Delivery/read confirmed A provider supplied the relevant receipt, if available

Do not label send acceptance as a read receipt. Do not label reply generation as delivery. Channel capabilities differ, so preserve “unknown” when you lack a delivery signal.

Use a stable inbound message identifier to detect redelivery. Use a separate execution attempt identifier for retries. Keep the conversation stable across related messages. These fields prevent a retry from looking like increased user engagement.

Enable native telemetry

For an existing Spectrum application, set the OTLP destination before startup and enable telemetry: true in its configuration:

Example scope: the code below is an integration adapter for an existing application. Configure its framework, model, credentials, and output contract as described; the demo screenshots were not generated by running these snippets.

export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://otel.agnost.ai/v1/traces"
export OTEL_EXPORTER_OTLP_HEADERS="X-Agnost-Org-ID=your-org-id"

Preserve your existing project credentials and provider configuration. Use Spectrum’s getting-started documentation for the framework setup and Agnost’s integration for destination details.

Inspect the exported records before relying on specific fields. Native telemetry is not a substitute for recording approved input/output content at your application’s reply boundary. Conversely, a conversation wrapper alone cannot explain all channel transport behavior.

Avoid cross-channel identity collisions

A provider user ID may be meaningful only within its platform or workspace. Namespace identifiers with the channel and tenant, or map them to your own verified application identity. Do not assume that two identical display names represent the same person.

Conversation identity may also depend on a room and thread, not only a user. In a shared channel, grouping every message by sender can split one thread; grouping by the whole channel can merge unrelated discussions. Choose the mapping that matches the actual product interaction.

If a user moves between surfaces, join identities only when you have a reliable link. Observability should preserve uncertainty instead of manufacturing a cross-channel user journey.

Diagnose the failures unique to messaging

The trace says success, but no reply appeared. Identify whether the recorded boundary ended at generation or send acceptance. Inspect the provider’s response and any available delivery receipt.

The user received two replies. Compare inbound message identifiers and attempt records. A webhook retry or redelivered event can trigger duplicate work unless processing is idempotent.

The answer used the wrong context. Inspect room/thread mapping and the context passed to the agent. A correct conversation label in Agnost does not configure the assistant’s memory.

One channel is much slower. Separate generation duration from channel send time and any queueing. Compare similar request types; a channel receiving longer messages may have a different workload.

Traces contain no conversation text. Add the application content boundary. Native transport telemetry alone is insufficient for transcript-based analysis.

Use conversation evidence for product decisions

In Agnost Users, review the incoming request and recorded reply together. Use Events for the execution evidence received. Configured intents can organize what people ask the assistant to do across supported surfaces.

When comparing channels, keep denominators consistent. Received messages, unique conversations, generated replies, and accepted sends are different counts. A high reply count may reflect retries rather than adoption. A lack of follow-up messages does not prove resolution.

Capture actual outcome evidence where the product has it: an accepted action, a corrected response, or an explicit handoff. Keep generation quality and transport reliability separate so a messaging outage does not get mistaken for a prompt problem.

Frequently asked questions

Is Photon the same thing as Spectrum?

Photon is the team and platform behind Spectrum. spectrum-ts is the TypeScript package used in this guide. Keep all three names together when locating its documentation or integrations.

How do I debug an iMessage agent that generated a reply but did not send it?

Identify the failed phase, inspect the provider result, and reconcile the outgoing message ID where available. A successful model call establishes generation only. A transport timeout does not establish that a resend is safe. Keep delivery unknown until the provider supplies evidence.

Does this example trace the model inside my agent?

No. It records the message handler’s input, output, elapsed time, and send state. Add instrumentation for the model or agent framework inside createReply when you need tokens, cost, or internal tool spans.

Does native telemetry include message bodies?

The native path documented by Agnost does not include transcript text. Add explicit tracking for approved content if you want to review conversations.

Can this monitor an assistant using any model?

The application wrapper observes the handler boundary regardless of how createReply is implemented. Model-specific tracing requires instrumentation for the model or framework inside that function.

Start with Agnost’s Spectrum-TS guide. If your reply generator streams with the AI SDK, see Vercel AI SDK monitoring for its separate completion lifecycle.

Browse all agent observability guides by framework.