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çenek | Ortam değişkeni | Varsayılan |
|---|---|---|
endpoint | MCPD_ENDPOINT | http://localhost:4317 |
apiKey | MCPD_API_KEY | yok |
project | MCPD_PROJECT | default |
environment | MCPD_ENVIRONMENT | yok |
serviceName | MCPD_SERVICE_NAME | yok |
serviceVersion | MCPD_SERVICE_VERSION | yok |
sample | MCPD_SAMPLE | 1.0 |
enabled | MCPD_ENABLED | true |
capturePayloads | MCPD_CAPTURE_PAYLOADS | true |
logLevel | MCPD_LOG_LEVEL | debug |
batchSize | MCPD_BATCH_SIZE | 128 |
flushIntervalMs | MCPD_FLUSH_INTERVAL_MS | 2000 |
maxQueue | MCPD_MAX_QUEUE | 8192 |
timeoutMs | MCPD_TIMEOUT_MS | 10000 |
sink | HTTP 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
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
maxQueuekadar sinyal tutar. Dolduğunda en eskisi atılır vemcpd.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
camelCaseile daemon'ınsnake_casebiçimi arasında dönüştürülür ve dönüşüminput,output,attributes,metadata,fieldsvelabelsalanları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.