apps/server/src/telemetry/ProviderDimensions.ts

import type {
  ModelSelection,
  OrchestrationV2ThreadShell,
  ServerProvider,
} from "@t3tools/contracts";

/**
 * Drivers whose model catalogs are published by the vendor. Other drivers
 * (OpenCode, Pi, ACP agents) list models from the user's own configuration,
 * such as local Ollama tags, so their model names stay out of analytics.
 */
const VENDOR_CATALOG_DRIVERS: ReadonlySet<string> = new Set([
  "codex",
  "claudeAgent",
  "cursor",
  "grok",
  "antigravity",
]);

export interface ProviderDimensions {
  readonly provider: string;
  readonly model?: string;
}

/**
 * Anonymous provider and model for a model selection. Instance ids are
 * user-defined, so only the driver kind is reported. The model is reported
 * only when it comes from a vendor catalog and is not a user-added custom model.
 */
export function providerDimensions(
  providers: ReadonlyArray<ServerProvider>,
  selection: Pick<ModelSelection, "instanceId" | "model">,
): ProviderDimensions {
  const provider = providers.find((candidate) => candidate.instanceId === selection.instanceId);
  if (provider === undefined) return { provider: "unknown" };
  const model = provider.models.find((candidate) => candidate.slug === selection.model);
  return VENDOR_CATALOG_DRIVERS.has(provider.driver) && model !== undefined && !model.isCustom
    ? { provider: provider.driver, model: model.slug }
    : { provider: provider.driver };
}

/** The thread fields agent analytics reads. Ids are used for lookups, never reported. */
export type AgentThread = Pick<
  OrchestrationV2ThreadShell,
  "modelSelection" | "runtimeMode" | "interactionMode" | "createdBy" | "creationSource" | "lineage"
>;

type Fields = Readonly<Record<string, unknown>>;

const asFields = (value: unknown): Fields =>
  typeof value === "object" && value !== null ? (value as Fields) : {};

const stringField = (fields: Fields, key: string) =>
  typeof fields[key] === "string" ? (fields[key] as string) : undefined;

/** A string from the tool input only when it is one of the schema's literals. */
const oneOf = (value: string | undefined, allowed: ReadonlyArray<string>) =>
  value !== undefined && allowed.includes(value) ? value : undefined;

/**
 * Whether two resolved providers differ. Unresolved selections compare as
 * unknown rather than as a cross-provider handoff.
 */
export const crossProvider = (caller: string, target: string) =>
  caller === "unknown" || target === "unknown" ? undefined : caller !== target;

const defined = (entries: Readonly<Record<string, unknown>>) =>
  Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== undefined));

/**
 * How deep a thread sits in a delegation tree: 0 for a top-level thread, 1 for
 * its subagent, and so on. Forks are top-level threads of their own. Depth is
 * counted by walking parents, capped so a corrupt lineage cannot loop.
 */
export const MAX_REPORTED_DEPTH = 5;

/**
 * Who started the work that made this call. Scheduled runs keep the task
 * creator's provenance, so the run's message marks them instead.
 */
function callerOrigin(input: {
  readonly thread: Pick<AgentThread, "createdBy">;
  readonly scheduledRun: boolean;
}) {
  if (input.scheduledRun) return "scheduler";
  if (input.thread.createdBy === "agent") return "agent";
  return input.thread.createdBy === "system" ? "system" : "user";
}

/**
 * Settings the agent chose for a handoff, read only from closed enums and
 * booleans in the tool's arguments and result. Prompts, titles, and ids are
 * never read.
 */
export function handoffSettings(tool: string, args: unknown, result: unknown): Fields {
  const input = asFields(args);
  const output = asFields(result);
  switch (tool) {
    case "delegate_task":
      return defined({
        targetChosen: input.target !== undefined,
        // Rejected calls are recorded too, so input strings must match a literal.
        mode: oneOf(stringField(input, "mode"), ["async", "wait"]) ?? "async",
        ...(output.waitTimedOut === true ? { waitTimedOut: true } : {}),
      });
    case "create_threads": {
      const threads = Array.isArray(input.threads) ? input.threads : [];
      return {
        batchSize: threads.length,
        targetChosen: threads.some((thread) => asFields(thread).target !== undefined),
      };
    }
    case "t3_thread_launch":
      return defined({
        targetChosen: input.modelSelection !== undefined,
        workspace:
          input.scratch === true
            ? "scratch"
            : (oneOf(stringField(asFields(input.workspaceStrategy), "type"), [
                "root",
                "worktree",
                "existing_worktree",
              ]) ?? "root"),
      });
    case "t3_thread_send":
      return defined({ delivery: stringField(output, "delivery") });
    case "t3_thread_send_attachments":
      return { delivery: "auto" };
    case "schedule_task":
      return { bindToCurrentThread: input.bindToCurrentThread !== false };
    default:
      return {};
  }
}

/** Outcome of a tool call, with the stable failure code when the call failed. */
export function toolOutcome(
  result:
    | { readonly isError?: boolean | undefined; readonly structuredContent?: unknown }
    | undefined,
) {
  if (result === undefined) return { outcome: "error", errorCode: "exception" };
  const content = asFields(result.structuredContent);
  // T3 tool failures are declared errors whose structured content carries a
  // closed `code`; the MCP layer does not always set isError for them.
  const code = stringField(content, "code") ?? stringField(asFields(content.error), "_tag");
  if (result.isError === true || (content._tag !== undefined && code !== undefined)) {
    return { outcome: "error", errorCode: code ?? "unknown" };
  }
  return { outcome: "ok" };
}

/**
 * Event properties for an agent tool call: the calling agent, and when the call
 * handed work to other threads, the agents running them. Each target counts
 * once, so a create_threads batch on two providers reports both.
 */
export function agentToolProperties(input: {
  readonly tool: string;
  readonly providers: ReadonlyArray<ServerProvider>;
  readonly caller: AgentThread | undefined;
  readonly callerProviderInstanceId?: ModelSelection["instanceId"];
  readonly callerDepth?: number;
  readonly callerScheduledRun?: boolean;
  readonly targets: ReadonlyArray<AgentThread>;
  readonly outcome: Fields;
  readonly settings?: Fields;
  readonly durationMs?: number;
}): ReadonlyArray<Readonly<Record<string, unknown>>> {
  const caller =
    input.caller !== undefined
      ? providerDimensions(input.providers, input.caller.modelSelection)
      : input.callerProviderInstanceId !== undefined
        ? {
            provider: providerDimensions(input.providers, {
              instanceId: input.callerProviderInstanceId,
              model: "",
            }).provider,
          }
        : { provider: "unknown" };
  const base = defined({
    tool: input.tool,
    callerProvider: caller.provider,
    callerModel: caller.model,
    callerOrigin:
      input.caller === undefined
        ? undefined
        : callerOrigin({
            thread: input.caller,
            scheduledRun: input.callerScheduledRun === true,
          }),
    callerDepth:
      input.callerDepth === undefined ? undefined : Math.min(input.callerDepth, MAX_REPORTED_DEPTH),
    durationMs: input.durationMs,
    ...input.outcome,
    ...input.settings,
  });
  if (input.targets.length === 0) return [base];
  return input.targets.map((thread) => {
    const target = providerDimensions(input.providers, thread.modelSelection);
    return defined({
      ...base,
      targetProvider: target.provider,
      targetModel: target.model,
      targetRuntimeMode: thread.runtimeMode,
      targetInteractionMode: thread.interactionMode,
      crossProvider: crossProvider(caller.provider, target.provider),
    });
  });
}