| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { mkdtemp, rm } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { join } from 'node:path'; |
|
|
| import { EXAMPLE_CLIENT_IDENTITY } from './identity.js'; |
|
|
|
|
| import { bootstrap, logSeed, resolveLoggingConfig } from '@moonshot-ai/agent-core-v2'; |
| import { createKlient } from '@moonshot-ai/klient/memory'; |
|
|
| const SEEDED_MODEL_ID = 'klient-example-model'; |
|
|
| interface TokenUsage { |
| inputOther: number; |
| output: number; |
| inputCacheRead: number; |
| inputCacheCreation: number; |
| } |
|
|
| function usageTotal(usage: TokenUsage | undefined): number | undefined { |
| if (usage === undefined) return undefined; |
| return usage.inputOther + usage.output + usage.inputCacheRead + usage.inputCacheCreation; |
| } |
|
|
| const tick = (ms: number): Promise<void> => |
| new Promise((resolve) => { |
| setTimeout(resolve, ms); |
| }); |
|
|
| async function main(): Promise<void> { |
| const seedModel = process.env['KIMI_EXAMPLE_MODEL']; |
| const seedKey = process.env['KIMI_EXAMPLE_API_KEY']; |
| if (seedModel === undefined || seedKey === undefined) { |
| throw new Error('KIMI_EXAMPLE_MODEL and KIMI_EXAMPLE_API_KEY are required (see header)'); |
| } |
|
|
| const homeDir = await mkdtemp(join(tmpdir(), 'klient-context-usage-')); |
| const { app } = bootstrap({ homeDir, clientIdentity: EXAMPLE_CLIENT_IDENTITY }, [ |
| ...logSeed(resolveLoggingConfig({ homeDir, env: process.env })), |
| ]); |
| try { |
| const klient = createKlient({ scope: app }); |
|
|
| const session = await klient.global.sessions.create({ workDir: process.cwd() }); |
| console.log('[session] created ->', session.id); |
| const agent = klient.session(session.id).agent('main'); |
|
|
| await klient.global.kosong.addProvider({ |
| id: SEEDED_MODEL_ID, |
| model: seedModel, |
| protocol: (process.env['KIMI_EXAMPLE_PROTOCOL'] ?? 'openai'), |
| baseUrl: process.env['KIMI_EXAMPLE_BASE_URL'] ?? 'http://127.0.0.1:1', |
| auth: { method: 'api-key', apiKey: seedKey }, |
| maxContextSize: 262_144, |
| }); |
| await agent.setModel(SEEDED_MODEL_ID); |
| console.log('[model] bound ->', await agent.getModel()); |
|
|
| const startedAt = Date.now(); |
| const elapsed = (): string => `+${String(Date.now() - startedAt).padStart(6)}ms`; |
|
|
| |
| agent.events.on('agent.status.updated', (event) => { |
| const slice: Record<string, unknown> = {}; |
| if ('contextTokens' in event) slice['contextTokens'] = event['contextTokens']; |
| if ('maxContextTokens' in event) slice['maxContextTokens'] = event['maxContextTokens']; |
| if ('contextUsage' in event) slice['contextUsage'] = event['contextUsage']; |
| if ('phase' in event) slice['phase'] = event['phase']; |
| const usage = event['usage'] as { total?: TokenUsage } | undefined; |
| if (usage !== undefined) slice['usage.total'] = usageTotal(usage.total); |
| console.log(`[event] ${elapsed()} agent.status.updated ->`, JSON.stringify(slice)); |
| }); |
| agent.events.on('turn.started', (event) => { |
| console.log(`[event] ${elapsed()} turn.started -> turnId=${String(event.turnId)}`); |
| }); |
| agent.events.on('turn.ended', (event) => { |
| console.log(`[event] ${elapsed()} turn.ended -> reason=${event.reason}`); |
| }); |
| agent.events.on('error', (event) => { |
| console.log(`[event] ${elapsed()} error ->`, JSON.stringify(event)); |
| }); |
| agent.events.onError((error) => { |
| console.log(`[event-err] ${elapsed()} ${error.message.split('\n')[0] ?? error.message}`); |
| }); |
|
|
| const completed = new Promise<'completed' | 'failed' | 'timeout'>((resolve) => { |
| const timer = setTimeout(() => { |
| sub.dispose(); |
| resolve('timeout'); |
| }, 120_000); |
| const sub = agent.events.on('prompt.completed', (event) => { |
| clearTimeout(timer); |
| sub.dispose(); |
| console.log( |
| `[event] ${elapsed()} prompt.completed -> reason=${event.reason ?? 'unknown'}`, |
| ); |
| resolve(event.reason === 'failed' ? 'failed' : 'completed'); |
| }); |
| }); |
|
|
| |
| let lastKey = ''; |
| const snapshot = async (tag: string): Promise<void> => { |
| const [ctx, usage] = await Promise.all([agent.getContext(), agent.getUsage()]); |
| const total = usageTotal(usage.total); |
| const turn = usageTotal(usage.currentTurn); |
| const key = `${String(ctx.history.length)}/${String(ctx.tokenCount)}/${String(total)}/${String(turn)}`; |
| if (key === lastKey) return; |
| lastKey = key; |
| console.log( |
| `[poll] ${elapsed()} ${tag}`.padEnd(46), |
| `history=${String(ctx.history.length)} tokenCount(measured)=${String(ctx.tokenCount)}` + |
| ` usage.total=${String(total)} usage.currentTurn=${String(turn)}`, |
| ); |
| }; |
|
|
| let polling = true; |
| const pollLoop = (async (): Promise<void> => { |
| while (polling) { |
| try { |
| await snapshot(''); |
| } catch { |
| |
| } |
| await tick(250); |
| } |
| })(); |
|
|
| await snapshot('created (pre-prompt)'); |
| console.log(`[prompt] ${elapsed()} sending "hi"`); |
| await agent.prompt({ input: [{ type: 'text', text: 'hi' }] }); |
|
|
| const outcome = await completed; |
| polling = false; |
| await pollLoop; |
| lastKey = ''; |
| await snapshot('after prompt.completed'); |
|
|
| const ctx = await agent.getContext(); |
| const usage = await agent.getUsage(); |
| const total = usageTotal(usage.total); |
| console.log('---'); |
| console.log('[result] outcome ->', outcome); |
| console.log('[result] history messages ->', ctx.history.length); |
| console.log('[result] tokenCount (measured) ->', ctx.tokenCount); |
| console.log('[result] usage.total ->', JSON.stringify(usage.total)); |
| console.log('[result] usage.byModel ->', JSON.stringify(usage.byModel)); |
| console.log( |
| `[check] tokenCount vs usage.total -> ${String(ctx.tokenCount)} vs ${String(total)}`, |
| ); |
| console.log( |
| '[note] reading guide:\n' + |
| ' - tokenCount is 0 until the first measured exchange lands, then it\n' + |
| ' should equal THAT exchange\'s total (input + output); new messages\n' + |
| ' appended between turns are the unmeasured tail.\n' + |
| ' - after one covered exchange on a fresh session, cumulative\n' + |
| ' usage.total and tokenCount should roughly agree; a large gap means\n' + |
| ' the measured total never made it onto the wire model and the reading\n' + |
| ' silently fell back to per-message estimates.\n' + |
| ' - outcome "timeout" means the turn finished its work but the\n' + |
| ' prompt.completed event never reached the client.', |
| ); |
| console.log('[note] session left in the (disposed) temp home ->', session.id); |
|
|
| await klient.close(); |
| if (outcome === 'failed') process.exit(1); |
| } finally { |
| app.dispose(); |
| await rm(homeDir, { recursive: true, force: true }); |
| } |
| } |
|
|
| try { |
| await main(); |
| } catch (error) { |
| console.error(error); |
| process.exit(1); |
| } |
|
|