İçeriğe geç
mcpd
daemon yok
Kaynak

TypeScript

Bağımlılığı olmayan tek bir paket. Span, log, metrik, puan ve geri bildirim kaydeder ve bir ölçümleme çağrısından hiçbir zaman hata fırlatmaz.

Kurulum

Node 20 veya üstü, Deno, Bun, tarayıcılar ve edge runtime'ları. Önce ESM, require için de bir CommonJS derlemesi. Node'a özgü tek parçalar olan async_hooks ve zlib çalışma anında yüklenir ve bulunmadıkları yerde atlanır.

npm install @mcpd/sdk

İstemciyi oluşturma

Süreç başına bir istemci. Her seçenek bir ortam değişkenini de okur, böylece aynı kod tek bir düzenleme olmadan hem geliştirmede hem üretimde çalışır. Hiç argüman verilmezse http://localhost:4317 adresindeki bir daemon ile konuşur.

bağlanma

import { Mcpd } from '@mcpd/sdk'

// Every field here also reads an environment variable, so the same code runs
// unchanged in development and in production. With no arguments at all this
// talks to a daemon on http://localhost:4317.
const mcpd = new Mcpd({
  project: 'support',
  environment: 'development',
  serviceName: 'support-agent',
  serviceVersion: '1.4.0',
})

Kod ortam değişkenine, ortam değişkeni de varsayılana üstün gelir.

SeçenekOrtam değişkeniVarsayılan
endpointMCPD_ENDPOINThttp://localhost:4317
apiKeyMCPD_API_KEYyok
projectMCPD_PROJECTdefault
environmentMCPD_ENVIRONMENTyok
serviceNameMCPD_SERVICE_NAMEyok
serviceVersionMCPD_SERVICE_VERSIONyok
sampleMCPD_SAMPLE1.0
enabledMCPD_ENABLEDtrue
capturePayloadsMCPD_CAPTURE_PAYLOADStrue
logLevelMCPD_LOG_LEVELdebug
batchSizeMCPD_BATCH_SIZE128
flushIntervalMsMCPD_FLUSH_INTERVAL_MS2000
maxQueueMCPD_MAX_QUEUE8192
timeoutMsMCPD_TIMEOUT_MS10000
sinkHTTP taşıyıcısı

enabled: false hiçbir şey kaydetmez ve hiçbir şey göndermez; yine de her çağrı geçerli bir nesne döndürür ve liste okumaları boş bir sayfa verir. Bir test takımı, ölçümlemenin tek satırına dokunmadan mcpd'yi kapatabilir.

capturePayloads: false prompt'ları ve tamamlamaları sürecin içinde tutar. Span yine de zamanlaması, durumu, token kullanımı, modeli, ilişkilendirmesi ve hatalarıyla kaydedilir; input ve output kuyruğa varmadan atılır, böylece bir flush sırasında hassas hiçbir şey dışarı çıkamaz. Bir span, başlangıç seçeneklerindeki capturePayloads ile bunu kendisi için iki yönde de geçersiz kılar.

Bir çalıştırmayı kaydetme

withSpan kapsamlı biçimdir. Span'i açar, callback'in başlattığı her şey için etkin tutar ve callback sonuçlandığında kapatır. Callback hata fırlatırsa span, fırlatılan mesajla birlikte error olarak işaretlenir ve hata olduğu gibi yeniden fırlatılır; yani gözleniyor olmak kendi hata işleyişinizi değiştirmez.

bir çalıştırmayı kaydetme

/**
 * One ticket, one trace.
 *
 * `withSpan` opens the span, keeps it active for everything the callback
 * starts, and ends it when the callback settles. Nothing inside has to be
 * passed a handle to be nested under it.
 */
async function handleTicket(ticket: Ticket): Promise<{ answer: string; traceId: string }> {
  let traceId = ''
  const answer = await mcpd.withSpan('support_agent', async (run) => {
    // Keep the id: a score or a piece of feedback that arrives later attaches
    // to this run by it.
    traceId = run.traceId
    // Tags label the whole run, so the trace list can filter on them.
    run.setTags(['support', 'tier-1'])
    run.setCorrelation({ sessionId: ticket.id, userId: ticket.customerId })
    run.setInput({ question: ticket.question })

    const order = await lookUpOrder(ticket)
    const answer = await draftAnswer(ticket, order)

    run.setOutput({ answer })
    return answer
  })
  return { answer, traceId }
}

Bağlam

Node ve Bun üzerinde etkin span bir AsyncLocalStorage içinde durur. Bir withSpan callback'i içinde başlatılan her şey, kendisine bir referans verilmeden onun altına yerleşir; tek bir olay döngüsünde iç içe geçen iki çalıştırma da birbirinin span'lerini hiç görmez.

iç içe geçme

// Callback'in başlattığı her şey onun altına yerleşir. Aşağıya referans geçilmez.
await mcpd.withSpan('support_agent', async (run) => {
  const order = await mcpd.withSpan(
    'lookup_order',
    async (tool) => {
      tool.setInput({ orderId: 'A-1042' })
      mcpd.log('info', 'this line belongs to lookup_order')
      return { status: 'in transit' }
    },
    { kind: 'tool_call' },
  )
  run.setOutput(order)
})

Tarayıcılarda ve çoğu edge runtime'ında AsyncLocalStorage yoktur. Oralarda SDK bunun yerine tek bir saklı değer tutar. Bu değer doğru iç içe geçer ve bir çalıştırmanın içindeki await ifadesini aşar, ama eşzamanlı iki çalıştırmayı birbirinden ayıramaz; sonuç, bir çalıştırmanın araç çağrısının başka bir çalıştırmanın ağacında görünmesi olur. Ajanları bu runtime'larda eşzamanlı çalıştırıyorsanız üst span'i açıkça geçirin.

üst span'i açıkça vermek

// Her runtime'da doğru, AsyncLocalStorage olmayanlarda da.
const run = mcpd.trace('support_agent')
const step = run.child('call_model', { kind: 'model_generation' })
step.end()

await run.withChild('call_tool', async (tool) => {
  tool.setOutput({ ok: true })
})
run.end()

// Çalışma anında hangi mekanizmanın kullanıldığı.
mcpd.stats().context // 'async-local-storage' | 'fallback'

Servisler arasında bağlam, W3C traceparent başlığında taşınır. Bir trace'in ağ üzerindeki bir sıçramayı atlatmasını sağlayan tek şey budur. Bozuk bir başlık yok sayılır ve yeni bir kök trace başlatılır; çünkü kimsenin okuyamadığı bir başlık, onu taşıyan isteği başarısız kılmak için bir sebep değildir.

servis sınırının ötesinde

// Giden: etkin span'i isteğe koyun.
await fetch(url, {
  headers: { traceparent: span.traceparent() },
})

// Gelen: yeni bir trace başlatmak yerine o trace'i sürdürün.
const run = mcpd.trace('handle_request', {
  traceparent: request.headers.get('traceparent'),
})

Model, kullanım ve hatalar

mcpd alım sırasında token sayılarını fiyatlandırır, dolayısıyla trace'te gördüğünüz maliyet size faturalanan maliyettir. Token'ları kendiniz saymak yerine sağlayıcının bildirdiğini raporlayın. Bir hata her zaman bir mesaj taşır ve hatayı vermek aynı hamlede span'in durumunu error yapar, böylece ikisi hiçbir zaman çelişemez.

bir span neler taşır

generation.setModel('claude-sonnet-4', 'anthropic')
generation.setUsage({
  inputTokens: 812,
  outputTokens: 37,
  inputDetails: { cacheRead: 640 },
})

// Kendiniz yakalayıp kendiniz işlediğiniz bir hata.
generation.setError('the provider timed out', {
  kind: 'TimeoutError',
  retryable: true,
})

// Span içinde bir an; kendine ait bir süresi yoktur.
generation.event('first_token', { kind: 'model_chunk' })

Kalıcı çalıştırmalar

Daemon'ın kalıcı çalıştırmalarını da aynı istemci karşılar. workflow() tanımı kurar ve adım gövdelerini saklar; bir Worker bu tanımı kaydeder, lease'ler için uzun yoklama yapar ve her birini karşılar; mcpd.runs, mcpd.events ve mcpd.definitions ise çalıştırma yüzeyini bire bir yansıtır. Bir gövde, adımın bütçesi içinde yeniden denenmek için RetryableError fırlatır ve StepContext üzerinden kendi adımının altına span kaydeder. Böyle kaydedilen bir span, altına yerleşen her span ile birlikte, bittiğinde kendi kullanımını adımınkine ekler; böylece daemon trace'in gösterdiğini fiyatlandırır ve gövde token'ları hiçbir zaman elle toplamaz; ctx.setUsage açık bir geçersiz kılmadır. onError yanında dört hook durur: onLease, onStepStart, onStepEnd ve onConflict; worker.stats() ise talepleri, tamamlamaları, sınıfına göre başarısızlıkları ve nedenine göre çakışmaları sayar.

bir iş akışı

const support = workflow({ id: 'support_ticket' })
  .step('classify', async (ctx, input: { ticket: string }) => ({
    kind: input.ticket.startsWith('Q') ? 'question' : 'bug',
  }))
  .agent(
    'draft',
    async (ctx, input: { kind: string }) => {
      // Your own model call, recorded under this step in the run's trace. Its
      // usage adds up into the step's, which the daemon prices.
      return await ctx.withSpan('model_call', async (span) => {
        span.setModel('claude-sonnet-4', 'anthropic')
        span.setUsage({ inputTokens: 812, outputTokens: 37 })
        return `an answer to a ${input.kind}`
      })
    },
    { input: { kind: '${steps.classify.output.kind}' } },
  )
  .waitFor('approval', {
    event: 'approved',
    key: '${input.ticket}',
    timeoutMs: 7 * 24 * 3_600_000,
    when: when('steps.classify.output.kind', 'eq', 'bug'),
  })
  .step(
    'send',
    async (ctx, input: { text: string }) => {
      if (!(await deliver(input.text, ctx.idempotencyKey))) {
        throw new RetryableError('mail relay busy', { retryAfterMs: 2_000 })
      }
      return { sent: true }
    },
    {
      input: { text: '${steps.draft.output}' },
      effect: 'at_most_once',
      retry: { maxAttempts: 3, backoff: 'exponential', initialMs: 500 },
    },
  )
  .output({ sent: '${steps.send.output.sent}' })

Her yapının ne yaptığı, lease protokolü ve çalıştırma API'si şu sayfalarda: İş akışları, Worker'lar ve Çalıştırmalar.

Puanlar ve geri bildirim

min ile max alanlarını tip zorunlu kılar. Bildirilmemiş bir ölçekteki 0.7'lik bir puan hiçbir şeyle karşılaştırılamaz ve biri grafiği okuduğunda bunun ne anlama geldiğini sormak için artık çok geçtir. Geri bildirim bir insanın yargısıdır ve puandan ayrı tutulur; çünkü ikisi farklı sorulara yanıt verir ve asla birlikte ortalanmamalıdır.

puan ve geri bildirim

// A machine evaluation of the run. `min` and `max` are required: 0.86 on an
// undeclared scale cannot be compared to anything.
mcpd.score({
  traceId,
  scorerId: 'answer_relevance',
  value: 0.86,
  min: 0,
  max: 1,
  reason: 'Answered the question and gave the delivery date.',
})

// A human evaluation. Kept separate from a score, and never averaged with
// one, because the two answer different questions.
mcpd.feedback({
  traceId,
  kind: 'thumbs',
  value: 'up',
  comment: 'Exactly what the customer asked for.',
  authorKind: 'end_user',
})

// Anything you already count can travel with the trace.
mcpd.metric('support.tickets_handled', 1, 'counter', { labels: { queue: 'tier-1' } })

Bir trace vermezseniz ikisi de etkin span'in trace'ine bağlanır; böylece kendi ürününüzdeki bir beğeni düğmesi, elinizde tuttuğunuz trace id'sinden başka bir şey vermeden feedback çağırabilir.

Beş sinyalin hepsi, tek bir kimlik kümesi

Span'ler, loglar, metrikler, puanlar ve geri bildirimler aynı kuyruktan geçer ve aynı trace ve span id'lerini taşır. Bir grafikteki sıçramadan onu doğuran çalıştırmayı açabilmenizi sağlayan şey budur.

  • Spans

    What ran, nested, with timings

  • Logs

    Lines the run emitted

  • Metrics

    Numbers, with exemplars back to a run

  • Scores

    Machine evaluation

  • Feedback

    What a person thought

  • Aspect notes

    A sentence a model wrote about the run

trace_id Store Search Export

Flush ve kapatma

Kayıt, kuyruğa ekleme demektir. Worker göndermeden hiçbir şey süreçten çıkmaz; bunu her iki saniyede bir ya da bir yığın dolduğunda yapar. flush() de shutdown() de beklenebilir ve idempotenttir; shutdown() ayrıca worker'ı durdurur.

Serverless bir fonksiyonda bu isteğe bağlı değildir. Runtime, siz bir yanıt döndürür döndürmez süreci dondurur ve worker bir daha çalışmaz, yani son yığın kaybolur. Yanıt çıkmadan önce bir flush bekleyin.

serverless bir handler

export default async function handler(request: Request): Promise<Response> {
  const answer = await mcpd.withSpan('answer', async (run) => {
    run.setInput(await request.text())
    return 'ok'
  })

  // Yığın burada hâlâ bellekte. Yanıt çıkmadan önce gönderin.
  await mcpd.flush()
  return new Response(answer)
}

Node üzerinde istemci beforeExit sırasında da flush eder; böylece shutdown() çağırmayı unutan bir betik yine de son yığınını gönderir. Bu hook açık bir process.exit() çağrısında veya ölümcül bir sinyalde tetiklenmez, yani bir emniyet ağıdır, shutdown() yerine geçmez.

Kaydettiğinizi doğrulama

RecordingSink ağın yerine belleği koyar; böylece bir test, sınanan kodun ürettiği span'leri, logları, metrikleri, puanları ve geri bildirimleri sunucu ve port olmadan geri okur. ConsoleSink bunun yerine sinyal başına tek bir JSON satırı yazar ve export(batch, ingestKey) metodu olan her nesne bir sink'tir. Hata fırlatan bir sink, başarısız bir yığın ve bir tanı kaydı demektir, kodunuzda asla bir istisna değil. mcpd.stats() şunları yanıtlar: queued, dropped, capacity, exported, failed ve rejected.

sunucu olmadan bir test

import { Mcpd, RecordingSink } from '@mcpd/sdk'

const sink = new RecordingSink()
const mcpd = new Mcpd({ sink })

await answerQuestion(mcpd)
await mcpd.flush()

expect(sink.spans.map((s) => s.name)).toEqual(['answer_question', 'model_call'])
expect(sink.scores[0]?.scorerId).toBe('answer_relevance')

Bitmiş bir span kaydetme

mcpd.record(span) kimliklerini, üstünü ve zamanlarını zaten elinizde tuttuğunuz bir span'i kuyruğa alır: başka bir izleme sistemi için yazılmış bir adaptörün, toplu bir içe aktarmanın veya bir yeniden oynatmanın elindeki şeyi. Span verildiği gibi kuyruğa girer; üstü, zamanları ve örneklemesi hiç değiştirilmez, yalnızca boş bıraktığı ilişkilendirme alanları istemciden doldurulur. Kimlikleri OpenTelemetry biçiminde olmayan, adı boş olan veya başlangıç zamanı eksik olan bir kayıt atılır ve stats().rejected içinde sayılır. Her span'de externalParentSpanId, mcpd olmayan bir izleme sisteminden gelen bir üstü yalnızca ilişkilendirme için taşır.

başka yerden gelen bir span

// Çağıranın elinde zaten olan kimlikler, üst ve zamanlar. Verildiği gibi kuyruğa girer.
mcpd.record({
  traceId: otel.traceId,
  spanId: otel.spanId,
  parentSpanId: otel.parentSpanId,
  name: 'GET /orders',
  kind: 'http_request',
  status: 'ok',
  startTime: otel.startTime,
  endTime: otel.endTime,
  isRoot: false,
  isEvent: false,
  correlation: { project: 'support' },
  receivedAt: otel.endTime,
})

// mcpd olmayan bir izleme sisteminden gelen bir üst, yalnızca ilişkilendirme için.
const run = mcpd.trace('handle_request', { externalParentSpanId: otel.spanId })

Güvenebilecekleriniz

  • Ölçümleme hiçbir zaman hata fırlatmaz. Başarısız bir dışa aktarım, erişilemeyen bir daemon veya serileştirilemeyen bir payload, onError üzerinden aralık başına en çok bir kez bildirilir ve sonra bırakılır. Gözlemlenebilirlik, gözlediği şeyi asla devirmemelidir.
  • Kayıt bloklamaz. Bir kuyruğa ekler ve döner; ağ işi worker'da olur, yani ajanınızın gecikmesi mcpd'nin hızlı ya da ayakta olmasına hiç bağlı değildir.
  • Bellek sınırlıdır. Kuyruk maxQueue kadar sinyal tutar. Dolduğunda en eskisi atılır ve mcpd.stats() içinde sayılır; çünkü sınırsız bir tampon, bir ağ kesintisini bellek yetersizliğinden ölüme çevirir.
  • Örnekleme kararı bir kez, kökte verilir ve aşağıya kalıtılır. Yarısı kaydedilmiş bir trace, sisteminizdeki bir hata gibi okunur.
  • Açık bir span de gönderilir. Bir span hem başladığında hem bittiğinde kuyruğa girer, yani yolun ortasında çöken bir çalıştırma da görünür olur. Başlangıç kaydı henüz çıkmamışsa bitiş kaydı onun yerini alır; böylece hızlı bir span iki değil bir kayda mal olur.
  • Payload'ınız hiçbir zaman yeniden yazılmaz. Anahtarlar camelCase ile daemon'ın snake_case biçimi arasında dönüştürülür ve dönüşüm input, output, attributes, metadata, fields ve labels alanlarında durur. Prompt'unuz tam olarak yazdığınız gibi ulaşır.

Geri okuma

Okumayı da aynı istemci yapar. Her metot bir HTTP endpoint'ini yansıtır ve üretilmiş tipleri döndürür. Okumalar başarısız olduğunda bir McpdError fırlatır: beklediğiniz ve yanıtlanamayan bir sorgu, dışa aktarılamayan bir span'in aksine, haber değeri taşır.

arama ve analiz

// The same client reads. This is the query that finds bad answers: every run
// that a scorer put below the bar.
const suspect = await mcpd.listTraces({
  hasScore: 'answer_relevance',
  scoreBelow: 0.5,
  limit: 10,
})
console.log(`runs scoring under 0.5: ${suspect.traces.length}`)

const recent = await mcpd.listTraces({ limit: 5, sort: 'start_time', order: 'desc' })
for (const trace of recent.traces) {
  const seconds = ((trace.durationNs ?? 0) / 1e9).toFixed(2)
  console.log(
    `${trace.traceId.slice(0, 8)}  ${trace.name.padEnd(16)} ${trace.status.padEnd(6)} ` +
      `${seconds}s  ${trace.spanCount} spans  $${(trace.cost?.totalCost ?? 0).toFixed(6)}`,
  )
}

// One run, with every span in it, ordered so a waterfall renders directly.
const detail = await mcpd.getTrace(traceId, { include: ['scores', 'feedback'] })
for (const span of detail.spans) {
  console.log(`  ${span.kind.padEnd(18)} ${span.name}`)
}

Çalıştırma bulma filtreleri eksiksiz anlatır. Veri modeli bu tiplerin taşıdığı her alanı listeler.