| ... | ... | @@ -14,6 +14,7 @@ import { |
| 14 | 14 | type PermissionResult, |
| 15 | 15 | type PermissionUpdate, |
| 16 | 16 | type SDKMessage, |
| 17 | type SDKControlGetContextUsageResponse, |
| 17 | 18 | type SDKResultMessage, |
| 18 | 19 | type SettingSource, |
| 19 | 20 | type SDKUserMessage, |
| ... | ... | @@ -29,6 +30,7 @@ import { |
| 29 | 30 | type ProviderApprovalDecision, |
| 30 | 31 | ProviderDriverKind, |
| 31 | 32 | ProviderInstanceId, |
| 33 | type ModelSelection, |
| 32 | 34 | ProviderItemId, |
| 33 | 35 | type ProviderRuntimeEvent, |
| 34 | 36 | type ProviderRuntimeTurnStatus, |
| ... | ... | @@ -159,6 +161,13 @@ interface ToolInFlight { |
| 159 | 161 | readonly lastEmittedInputFingerprint?: string; |
| 160 | 162 | } |
| 161 | 163 | |
| 164 | interface ClaudeTaskState { |
| 165 | readonly id: string; |
| 166 | subject: string; |
| 167 | status: PlanStep["status"]; |
| 168 | readonly blockedBy: Set<string>; |
| 169 | } |
| 170 | |
| 162 | 171 | interface ClaudeSessionContext { |
| 163 | 172 | session: ProviderSession; |
| 164 | 173 | readonly promptQueue: Queue.Queue<PromptQueueItem>; |
| ... | ... | @@ -175,9 +184,11 @@ interface ClaudeSessionContext { |
| 175 | 184 | items: Array<unknown>; |
| 176 | 185 | }>; |
| 177 | 186 | readonly inFlightTools: Map<number, ToolInFlight>; |
| 187 | readonly claudeTasks: Map<string, ClaudeTaskState>; |
| 178 | 188 | turnState: ClaudeTurnState | undefined; |
| 179 | 189 | lastKnownContextWindow: number | undefined; |
| 180 | 190 | lastKnownTokenUsage: ThreadTokenUsageSnapshot | undefined; |
| 191 | lastKnownTotalProcessedTokens: number | undefined; |
| 181 | 192 | lastAssistantUuid: string | undefined; |
| 182 | 193 | lastThreadStartedId: string | undefined; |
| 183 | 194 | stopped: boolean; |
| ... | ... | @@ -188,6 +199,7 @@ interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> { |
| 188 | 199 | readonly setModel: (model?: string) => Promise<void>; |
| 189 | 200 | readonly setPermissionMode: (mode: PermissionMode) => Promise<void>; |
| 190 | 201 | readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>; |
| 202 | readonly getContextUsage?: () => Promise<SDKControlGetContextUsageResponse>; |
| 191 | 203 | readonly close: () => void; |
| 192 | 204 | } |
| 193 | 205 | |
| ... | ... | @@ -337,60 +349,220 @@ function maxClaudeContextWindowFromModelUsage( |
| 337 | 349 | return maxContextWindow; |
| 338 | 350 | } |
| 339 | 351 | |
| 340 | | function normalizeClaudeTokenUsage( |
| 341 | | value: unknown, |
| 342 | | contextWindow?: number, |
| 343 | | ): ThreadTokenUsageSnapshot | undefined { |
| 344 | | if (!value || typeof value !== "object") { |
| 352 | function selectedClaudeContextWindow( |
| 353 | modelSelection: ModelSelection | undefined, |
| 354 | ): number | undefined { |
| 355 | switch (modelSelection?.model) { |
| 356 | case "claude-opus-4-8": |
| 357 | case "claude-opus-4-7": |
| 358 | return 1_000_000; |
| 359 | } |
| 360 | |
| 361 | const optionValue = getModelSelectionStringOptionValue(modelSelection, "contextWindow"); |
| 362 | if (optionValue === "1m") { |
| 363 | return 1_000_000; |
| 364 | } |
| 365 | if (optionValue === "200k") { |
| 366 | return 200_000; |
| 367 | } |
| 368 | const caps = getClaudeModelCapabilities(modelSelection?.model); |
| 369 | const hasContextWindowOption = getProviderOptionDescriptors({ caps }).some( |
| 370 | (descriptor) => descriptor.type === "select" && descriptor.id === "contextWindow", |
| 371 | ); |
| 372 | if (hasContextWindowOption) { |
| 373 | return 200_000; |
| 374 | } |
| 375 | return undefined; |
| 376 | } |
| 377 | |
| 378 | function finiteNonNegativeInteger(value: unknown): number | undefined { |
| 379 | return typeof value === "number" && Number.isFinite(value) && value >= 0 |
| 380 | ? Math.round(value) |
| 381 | : undefined; |
| 382 | } |
| 383 | |
| 384 | function finitePositiveInteger(value: unknown): number | undefined { |
| 385 | return typeof value === "number" && Number.isFinite(value) && value > 0 |
| 386 | ? Math.round(value) |
| 387 | : undefined; |
| 388 | } |
| 389 | |
| 390 | function claudeUsageInputTokens(usage: Record<string, unknown>): number { |
| 391 | return ( |
| 392 | (finiteNonNegativeInteger(usage.input_tokens) ?? 0) + |
| 393 | (finiteNonNegativeInteger(usage.cache_creation_input_tokens) ?? 0) + |
| 394 | (finiteNonNegativeInteger(usage.cache_read_input_tokens) ?? 0) |
| 395 | ); |
| 396 | } |
| 397 | |
| 398 | function claudeUsageOutputTokens(usage: Record<string, unknown>): number { |
| 399 | return finiteNonNegativeInteger(usage.output_tokens) ?? 0; |
| 400 | } |
| 401 | |
| 402 | function lastClaudeUsageIteration( |
| 403 | value: Record<string, unknown>, |
| 404 | ): Record<string, unknown> | undefined { |
| 405 | const iterations = Array.isArray(value.iterations) ? value.iterations : []; |
| 406 | return iterations.findLast( |
| 407 | (iteration): iteration is Record<string, unknown> => |
| 408 | iteration !== null && typeof iteration === "object" && !Array.isArray(iteration), |
| 409 | ); |
| 410 | } |
| 411 | |
| 412 | function claudeTotalProcessedTokens(value: unknown): number | undefined { |
| 413 | if (!value || typeof value !== "object" || Array.isArray(value)) { |
| 345 | 414 | return undefined; |
| 346 | 415 | } |
| 347 | 416 | |
| 348 | 417 | const usage = value as Record<string, unknown>; |
| 349 | | const inputTokens = |
| 350 | | (typeof usage.input_tokens === "number" && Number.isFinite(usage.input_tokens) |
| 351 | | ? usage.input_tokens |
| 352 | | : 0) + |
| 353 | | (typeof usage.cache_creation_input_tokens === "number" && |
| 354 | | Number.isFinite(usage.cache_creation_input_tokens) |
| 355 | | ? usage.cache_creation_input_tokens |
| 356 | | : 0) + |
| 357 | | (typeof usage.cache_read_input_tokens === "number" && |
| 358 | | Number.isFinite(usage.cache_read_input_tokens) |
| 359 | | ? usage.cache_read_input_tokens |
| 360 | | : 0); |
| 361 | | const outputTokens = |
| 362 | | typeof usage.output_tokens === "number" && Number.isFinite(usage.output_tokens) |
| 363 | | ? usage.output_tokens |
| 364 | | : 0; |
| 365 | | const derivedTotalProcessedTokens = inputTokens + outputTokens; |
| 366 | | const totalProcessedTokens = |
| 367 | | (typeof usage.total_tokens === "number" && Number.isFinite(usage.total_tokens) |
| 368 | | ? usage.total_tokens |
| 369 | | : undefined) ?? (derivedTotalProcessedTokens > 0 ? derivedTotalProcessedTokens : undefined); |
| 370 | | if (totalProcessedTokens === undefined || totalProcessedTokens <= 0) { |
| 418 | const explicitTotal = finiteNonNegativeInteger(usage.total_tokens); |
| 419 | if (explicitTotal !== undefined && explicitTotal > 0) { |
| 420 | return explicitTotal; |
| 421 | } |
| 422 | |
| 423 | const total = claudeUsageInputTokens(usage) + claudeUsageOutputTokens(usage); |
| 424 | return total > 0 ? total : undefined; |
| 425 | } |
| 426 | |
| 427 | function makeClaudeTokenUsageSnapshot(input: { |
| 428 | readonly activeTokens: number; |
| 429 | readonly inputTokens?: number; |
| 430 | readonly outputTokens?: number; |
| 431 | readonly contextWindow?: number; |
| 432 | readonly totalProcessedTokens?: number; |
| 433 | readonly lastUsedTokens?: number; |
| 434 | readonly compactsAutomatically?: boolean; |
| 435 | }): ThreadTokenUsageSnapshot | undefined { |
| 436 | const activeTokens = finiteNonNegativeInteger(input.activeTokens); |
| 437 | if (activeTokens === undefined || activeTokens <= 0) { |
| 371 | 438 | return undefined; |
| 372 | 439 | } |
| 373 | 440 | |
| 374 | | const maxTokens = |
| 375 | | typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0 |
| 376 | | ? contextWindow |
| 377 | | : undefined; |
| 378 | | const usedTokens = |
| 379 | | maxTokens !== undefined ? Math.min(totalProcessedTokens, maxTokens) : totalProcessedTokens; |
| 441 | const maxTokens = finitePositiveInteger(input.contextWindow); |
| 442 | const usedTokens = maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens; |
| 443 | const lastUsedTokens = |
| 444 | finiteNonNegativeInteger(input.lastUsedTokens) ?? |
| 445 | (maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens); |
| 446 | const totalProcessedTokens = finiteNonNegativeInteger(input.totalProcessedTokens); |
| 447 | const inputTokens = finiteNonNegativeInteger(input.inputTokens); |
| 448 | const outputTokens = finiteNonNegativeInteger(input.outputTokens); |
| 380 | 449 | |
| 381 | 450 | return { |
| 382 | 451 | usedTokens, |
| 383 | | lastUsedTokens: usedTokens, |
| 384 | | ...(totalProcessedTokens > usedTokens ? { totalProcessedTokens } : {}), |
| 385 | | ...(inputTokens > 0 ? { inputTokens } : {}), |
| 386 | | ...(outputTokens > 0 ? { outputTokens } : {}), |
| 452 | lastUsedTokens, |
| 453 | ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens |
| 454 | ? { totalProcessedTokens } |
| 455 | : {}), |
| 456 | ...(inputTokens !== undefined && inputTokens > 0 ? { inputTokens } : {}), |
| 457 | ...(outputTokens !== undefined && outputTokens > 0 ? { outputTokens } : {}), |
| 387 | 458 | ...(maxTokens !== undefined ? { maxTokens } : {}), |
| 388 | | ...(typeof usage.tool_uses === "number" && Number.isFinite(usage.tool_uses) |
| 389 | | ? { toolUses: usage.tool_uses } |
| 459 | ...(input.compactsAutomatically !== undefined |
| 460 | ? { compactsAutomatically: input.compactsAutomatically } |
| 390 | 461 | : {}), |
| 391 | | ...(typeof usage.duration_ms === "number" && Number.isFinite(usage.duration_ms) |
| 392 | | ? { durationMs: usage.duration_ms } |
| 462 | }; |
| 463 | } |
| 464 | |
| 465 | function normalizeClaudeActiveTokenUsage( |
| 466 | value: unknown, |
| 467 | contextWindow?: number, |
| 468 | totalProcessedTokens?: number, |
| 469 | ): ThreadTokenUsageSnapshot | undefined { |
| 470 | if (!value || typeof value !== "object") { |
| 471 | return undefined; |
| 472 | } |
| 473 | |
| 474 | const usage = value as Record<string, unknown>; |
| 475 | const activeUsage = lastClaudeUsageIteration(usage) ?? usage; |
| 476 | const inputTokens = claudeUsageInputTokens(activeUsage); |
| 477 | const outputTokens = claudeUsageOutputTokens(activeUsage); |
| 478 | const activeTokens = claudeTotalProcessedTokens(activeUsage) ?? inputTokens + outputTokens; |
| 479 | if (activeTokens <= 0) { |
| 480 | return undefined; |
| 481 | } |
| 482 | |
| 483 | return makeClaudeTokenUsageSnapshot({ |
| 484 | activeTokens, |
| 485 | inputTokens, |
| 486 | outputTokens, |
| 487 | ...(contextWindow !== undefined ? { contextWindow } : {}), |
| 488 | ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}), |
| 489 | }); |
| 490 | } |
| 491 | |
| 492 | function normalizeClaudeContextUsageApiSnapshot( |
| 493 | value: SDKControlGetContextUsageResponse, |
| 494 | totalProcessedTokens?: number, |
| 495 | ): ThreadTokenUsageSnapshot | undefined { |
| 496 | return makeClaudeTokenUsageSnapshot({ |
| 497 | activeTokens: value.totalTokens, |
| 498 | contextWindow: value.maxTokens, |
| 499 | ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}), |
| 500 | compactsAutomatically: value.isAutoCompactEnabled, |
| 501 | }); |
| 502 | } |
| 503 | |
| 504 | function compactBoundaryTokenUsageSnapshot( |
| 505 | message: Record<string, unknown>, |
| 506 | contextWindow?: number, |
| 507 | totalProcessedTokens?: number, |
| 508 | ): ThreadTokenUsageSnapshot | undefined { |
| 509 | const metadata = message.compact_metadata; |
| 510 | if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { |
| 511 | return undefined; |
| 512 | } |
| 513 | |
| 514 | const compactMetadata = metadata as Record<string, unknown>; |
| 515 | const postTokens = finiteNonNegativeInteger(compactMetadata.post_tokens); |
| 516 | if (postTokens === undefined || postTokens <= 0) { |
| 517 | return undefined; |
| 518 | } |
| 519 | |
| 520 | const preTokens = finiteNonNegativeInteger(compactMetadata.pre_tokens); |
| 521 | return makeClaudeTokenUsageSnapshot({ |
| 522 | activeTokens: postTokens, |
| 523 | ...(preTokens !== undefined ? { lastUsedTokens: preTokens } : {}), |
| 524 | ...(contextWindow !== undefined ? { contextWindow } : {}), |
| 525 | ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}), |
| 526 | }); |
| 527 | } |
| 528 | |
| 529 | function normalizeClaudeTaskProgressTokenUsage( |
| 530 | value: unknown, |
| 531 | context: ClaudeSessionContext, |
| 532 | ): ThreadTokenUsageSnapshot | undefined { |
| 533 | const totalTokens = claudeTotalProcessedTokens(value); |
| 534 | if (totalTokens === undefined || totalTokens <= 0) { |
| 535 | return undefined; |
| 536 | } |
| 537 | |
| 538 | const lastUsedTokens = context.lastKnownTokenUsage?.usedTokens; |
| 539 | const activeTokens = |
| 540 | lastUsedTokens !== undefined ? Math.max(totalTokens, lastUsedTokens) : totalTokens; |
| 541 | if (lastUsedTokens !== undefined && activeTokens === lastUsedTokens) { |
| 542 | return undefined; |
| 543 | } |
| 544 | |
| 545 | const usage = value as Record<string, unknown>; |
| 546 | const snapshot = makeClaudeTokenUsageSnapshot({ |
| 547 | activeTokens, |
| 548 | ...(context.lastKnownContextWindow !== undefined |
| 549 | ? { contextWindow: context.lastKnownContextWindow } |
| 393 | 550 | : {}), |
| 551 | totalProcessedTokens: Math.max( |
| 552 | totalTokens, |
| 553 | context.lastKnownTotalProcessedTokens ?? totalTokens, |
| 554 | ), |
| 555 | }); |
| 556 | if (!snapshot) { |
| 557 | return undefined; |
| 558 | } |
| 559 | |
| 560 | const toolUses = finiteNonNegativeInteger(usage.tool_uses); |
| 561 | const durationMs = finiteNonNegativeInteger(usage.duration_ms); |
| 562 | return { |
| 563 | ...snapshot, |
| 564 | ...(toolUses !== undefined ? { toolUses } : {}), |
| 565 | ...(durationMs !== undefined ? { durationMs } : {}), |
| 394 | 566 | }; |
| 395 | 567 | } |
| 396 | 568 | |
| ... | ... | @@ -539,6 +711,140 @@ function extractPlanStepsFromTodoInput(input: Record<string, unknown>): PlanStep |
| 539 | 711 | })); |
| 540 | 712 | } |
| 541 | 713 | |
| 714 | function isClaudeTaskTool(toolName: string): boolean { |
| 715 | return toolName === "TaskCreate" || toolName === "TaskUpdate" || toolName === "TaskList"; |
| 716 | } |
| 717 | |
| 718 | function normalizeClaudeTaskStatus(value: unknown): PlanStep["status"] { |
| 719 | return value === "completed" ? "completed" : value === "in_progress" ? "inProgress" : "pending"; |
| 720 | } |
| 721 | |
| 722 | function readString(value: unknown): string | undefined { |
| 723 | return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; |
| 724 | } |
| 725 | |
| 726 | function readStringArray(value: unknown): Array<string> { |
| 727 | return Array.isArray(value) |
| 728 | ? value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0) |
| 729 | : []; |
| 730 | } |
| 731 | |
| 732 | function readClaudeToolUseResult(message: SDKMessage): Record<string, unknown> | undefined { |
| 733 | if (message.type !== "user") { |
| 734 | return undefined; |
| 735 | } |
| 736 | const result = (message as { readonly tool_use_result?: unknown }).tool_use_result; |
| 737 | return result !== null && typeof result === "object" && !Array.isArray(result) |
| 738 | ? (result as Record<string, unknown>) |
| 739 | : undefined; |
| 740 | } |
| 741 | |
| 742 | function readClaudeTaskFromResult( |
| 743 | result: Record<string, unknown> | undefined, |
| 744 | ): Record<string, unknown> | undefined { |
| 745 | const task = result?.task; |
| 746 | return task !== null && typeof task === "object" && !Array.isArray(task) |
| 747 | ? (task as Record<string, unknown>) |
| 748 | : undefined; |
| 749 | } |
| 750 | |
| 751 | function applyClaudeTaskToolResult( |
| 752 | tasks: Map<string, ClaudeTaskState>, |
| 753 | tool: ToolInFlight, |
| 754 | result: Record<string, unknown> | undefined, |
| 755 | ): boolean { |
| 756 | if (!isClaudeTaskTool(tool.toolName)) { |
| 757 | return false; |
| 758 | } |
| 759 | |
| 760 | let changed = false; |
| 761 | if (tool.toolName === "TaskList") { |
| 762 | const resultTasks = result?.tasks; |
| 763 | if (!Array.isArray(resultTasks)) { |
| 764 | return false; |
| 765 | } |
| 766 | tasks.clear(); |
| 767 | for (const entry of resultTasks) { |
| 768 | if (entry === null || typeof entry !== "object" || Array.isArray(entry)) { |
| 769 | continue; |
| 770 | } |
| 771 | const task = entry as Record<string, unknown>; |
| 772 | const id = readString(task.id); |
| 773 | const subject = readString(task.subject); |
| 774 | if (!id || !subject) { |
| 775 | continue; |
| 776 | } |
| 777 | tasks.set(id, { |
| 778 | id, |
| 779 | subject, |
| 780 | status: normalizeClaudeTaskStatus(task.status), |
| 781 | blockedBy: new Set(readStringArray(task.blockedBy)), |
| 782 | }); |
| 783 | } |
| 784 | return tasks.size > 0; |
| 785 | } |
| 786 | |
| 787 | if (tool.toolName === "TaskCreate") { |
| 788 | const resultTask = readClaudeTaskFromResult(result); |
| 789 | const id = readString(resultTask?.id); |
| 790 | const subject = readString(resultTask?.subject) ?? readString(tool.input.subject); |
| 791 | if (!id || !subject) { |
| 792 | return false; |
| 793 | } |
| 794 | tasks.set(id, { |
| 795 | id, |
| 796 | subject, |
| 797 | status: normalizeClaudeTaskStatus(tool.input.status), |
| 798 | blockedBy: new Set(readStringArray(tool.input.blockedBy)), |
| 799 | }); |
| 800 | return true; |
| 801 | } |
| 802 | |
| 803 | const taskId = readString(tool.input.taskId) ?? readString(result?.taskId); |
| 804 | if (!taskId) { |
| 805 | return false; |
| 806 | } |
| 807 | const task = tasks.get(taskId); |
| 808 | if (!task) { |
| 809 | return false; |
| 810 | } |
| 811 | const subject = readString(tool.input.subject); |
| 812 | if (subject && task.subject !== subject) { |
| 813 | task.subject = subject; |
| 814 | changed = true; |
| 815 | } |
| 816 | if (typeof tool.input.status === "string") { |
| 817 | const status = normalizeClaudeTaskStatus(tool.input.status); |
| 818 | if (task.status !== status) { |
| 819 | task.status = status; |
| 820 | changed = true; |
| 821 | } |
| 822 | } |
| 823 | for (const dependency of readStringArray(tool.input.addBlockedBy)) { |
| 824 | if (!task.blockedBy.has(dependency)) { |
| 825 | task.blockedBy.add(dependency); |
| 826 | changed = true; |
| 827 | } |
| 828 | } |
| 829 | for (const dependency of readStringArray(tool.input.removeBlockedBy)) { |
| 830 | if (task.blockedBy.delete(dependency)) { |
| 831 | changed = true; |
| 832 | } |
| 833 | } |
| 834 | return changed; |
| 835 | } |
| 836 | |
| 837 | function planStepsFromClaudeTasks(tasks: Map<string, ClaudeTaskState>): PlanStep[] { |
| 838 | return Array.from(tasks.values()).map((task) => { |
| 839 | const blockedBy = Array.from(task.blockedBy); |
| 840 | const blockedSuffix = blockedBy.length > 0 ? ` (blocked by #${blockedBy.join(", #")})` : ""; |
| 841 | return { |
| 842 | step: `${task.subject}${blockedSuffix}`, |
| 843 | status: task.status, |
| 844 | }; |
| 845 | }); |
| 846 | } |
| 847 | |
| 542 | 848 | function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string { |
| 543 | 849 | const commandValue = input.command ?? input.cmd; |
| 544 | 850 | const command = typeof commandValue === "string" ? commandValue : undefined; |
| ... | ... | @@ -1438,6 +1744,70 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1438 | 1744 | }); |
| 1439 | 1745 | }); |
| 1440 | 1746 | |
| 1747 | const emitThreadTokenUsage = Effect.fn("emitThreadTokenUsage")(function* ( |
| 1748 | context: ClaudeSessionContext, |
| 1749 | usage: ThreadTokenUsageSnapshot | undefined, |
| 1750 | options?: { |
| 1751 | readonly rawMethod?: string; |
| 1752 | readonly rawPayload?: unknown; |
| 1753 | }, |
| 1754 | ) { |
| 1755 | if (!usage) { |
| 1756 | return; |
| 1757 | } |
| 1758 | |
| 1759 | context.lastKnownTokenUsage = usage; |
| 1760 | context.lastKnownTotalProcessedTokens = |
| 1761 | usage.totalProcessedTokens ?? context.lastKnownTotalProcessedTokens; |
| 1762 | |
| 1763 | const turnState = context.turnState; |
| 1764 | const stamp = yield* makeEventStamp(); |
| 1765 | yield* offerRuntimeEvent({ |
| 1766 | type: "thread.token-usage.updated", |
| 1767 | eventId: stamp.eventId, |
| 1768 | provider: PROVIDER, |
| 1769 | createdAt: stamp.createdAt, |
| 1770 | threadId: context.session.threadId, |
| 1771 | ...(turnState ? { turnId: turnState.turnId } : {}), |
| 1772 | payload: { |
| 1773 | usage, |
| 1774 | }, |
| 1775 | providerRefs: nativeProviderRefs(context), |
| 1776 | ...(options?.rawMethod || options?.rawPayload |
| 1777 | ? { |
| 1778 | raw: { |
| 1779 | source: "claude.sdk.message" as const, |
| 1780 | ...(options.rawMethod ? { method: options.rawMethod } : {}), |
| 1781 | payload: options.rawPayload, |
| 1782 | }, |
| 1783 | } |
| 1784 | : {}), |
| 1785 | }); |
| 1786 | }); |
| 1787 | |
| 1788 | const queryCurrentContextUsage = Effect.fn("queryCurrentContextUsage")(function* ( |
| 1789 | context: ClaudeSessionContext, |
| 1790 | totalProcessedTokens?: number, |
| 1791 | ) { |
| 1792 | if (!context.query.getContextUsage) { |
| 1793 | return undefined; |
| 1794 | } |
| 1795 | |
| 1796 | const usage = yield* Effect.promise(async () => { |
| 1797 | try { |
| 1798 | return await context.query.getContextUsage?.(); |
| 1799 | } catch { |
| 1800 | return undefined; |
| 1801 | } |
| 1802 | }); |
| 1803 | if (!usage) { |
| 1804 | return undefined; |
| 1805 | } |
| 1806 | |
| 1807 | context.lastKnownContextWindow = usage.maxTokens; |
| 1808 | return normalizeClaudeContextUsageApiSnapshot(usage, totalProcessedTokens); |
| 1809 | }); |
| 1810 | |
| 1441 | 1811 | const emitProposedPlanCompleted = Effect.fn("emitProposedPlanCompleted")(function* ( |
| 1442 | 1812 | context: ClaudeSessionContext, |
| 1443 | 1813 | input: { |
| ... | ... | @@ -1485,6 +1855,42 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1485 | 1855 | }); |
| 1486 | 1856 | }); |
| 1487 | 1857 | |
| 1858 | const emitClaudeTaskPlanUpdated = Effect.fn("emitClaudeTaskPlanUpdated")(function* ( |
| 1859 | context: ClaudeSessionContext, |
| 1860 | input: { |
| 1861 | readonly toolUseId: string; |
| 1862 | readonly rawMethod: string; |
| 1863 | readonly rawPayload: unknown; |
| 1864 | }, |
| 1865 | ) { |
| 1866 | const plan = planStepsFromClaudeTasks(context.claudeTasks); |
| 1867 | if (plan.length === 0) { |
| 1868 | return; |
| 1869 | } |
| 1870 | |
| 1871 | const stamp = yield* makeEventStamp(); |
| 1872 | yield* offerRuntimeEvent({ |
| 1873 | type: "turn.plan.updated", |
| 1874 | eventId: stamp.eventId, |
| 1875 | provider: PROVIDER, |
| 1876 | createdAt: stamp.createdAt, |
| 1877 | threadId: context.session.threadId, |
| 1878 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1879 | payload: { |
| 1880 | explanation: "Claude Tasks", |
| 1881 | plan, |
| 1882 | }, |
| 1883 | providerRefs: nativeProviderRefs(context, { |
| 1884 | providerItemId: input.toolUseId, |
| 1885 | }), |
| 1886 | raw: { |
| 1887 | source: "claude.sdk.message", |
| 1888 | method: input.rawMethod, |
| 1889 | payload: input.rawPayload, |
| 1890 | }, |
| 1891 | }); |
| 1892 | }); |
| 1893 | |
| 1488 | 1894 | const completeTurn = Effect.fn("completeTurn")(function* ( |
| 1489 | 1895 | context: ClaudeSessionContext, |
| 1490 | 1896 | status: ProviderRuntimeTurnStatus, |
| ... | ... | @@ -1496,51 +1902,77 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1496 | 1902 | context.lastKnownContextWindow = resultContextWindow; |
| 1497 | 1903 | } |
| 1498 | 1904 | |
| 1499 | | // The SDK result.usage contains *accumulated* totals across all API calls |
| 1500 | | // (input_tokens, cache_read_input_tokens, etc. summed over every request). |
| 1501 | | // This does NOT represent the current context window size. |
| 1502 | | // Instead, use the last known context-window-accurate usage from task_progress |
| 1503 | | // events and treat the accumulated total as totalProcessedTokens. |
| 1504 | | const accumulatedSnapshot = normalizeClaudeTokenUsage( |
| 1505 | | result?.usage, |
| 1506 | | resultContextWindow ?? context.lastKnownContextWindow, |
| 1905 | const maxTokens = resultContextWindow ?? context.lastKnownContextWindow; |
| 1906 | const accumulatedTotalProcessedTokens = claudeTotalProcessedTokens(result?.usage); |
| 1907 | if (accumulatedTotalProcessedTokens !== undefined) { |
| 1908 | context.lastKnownTotalProcessedTokens = accumulatedTotalProcessedTokens; |
| 1909 | } |
| 1910 | |
| 1911 | const contextUsageSnapshot = yield* queryCurrentContextUsage( |
| 1912 | context, |
| 1913 | accumulatedTotalProcessedTokens ?? context.lastKnownTotalProcessedTokens, |
| 1507 | 1914 | ); |
| 1508 | | const accumulatedTotalProcessedTokens = |
| 1509 | | accumulatedSnapshot?.totalProcessedTokens ?? accumulatedSnapshot?.usedTokens; |
| 1915 | const resultUsageRecord = |
| 1916 | result?.usage && typeof result.usage === "object" && !Array.isArray(result.usage) |
| 1917 | ? (result.usage as Record<string, unknown>) |
| 1918 | : undefined; |
| 1919 | const hasResultUsageIteration = |
| 1920 | resultUsageRecord !== undefined && lastClaudeUsageIteration(resultUsageRecord) !== undefined; |
| 1921 | const resultHasActiveUsage = |
| 1922 | resultUsageRecord !== undefined && |
| 1923 | (hasResultUsageIteration || |
| 1924 | claudeUsageInputTokens(resultUsageRecord) + claudeUsageOutputTokens(resultUsageRecord) > 0); |
| 1925 | const resultTotalOnly = |
| 1926 | resultUsageRecord !== undefined && |
| 1927 | !resultHasActiveUsage && |
| 1928 | claudeTotalProcessedTokens(resultUsageRecord) !== undefined; |
| 1929 | const resultIterationSnapshot = resultUsageRecord |
| 1930 | ? normalizeClaudeActiveTokenUsage( |
| 1931 | resultUsageRecord, |
| 1932 | maxTokens, |
| 1933 | accumulatedTotalProcessedTokens ?? context.lastKnownTotalProcessedTokens, |
| 1934 | ) |
| 1935 | : undefined; |
| 1510 | 1936 | const lastGoodUsage = context.lastKnownTokenUsage; |
| 1511 | | const maxTokens = resultContextWindow ?? context.lastKnownContextWindow; |
| 1512 | | const usageSnapshot: ThreadTokenUsageSnapshot | undefined = lastGoodUsage |
| 1513 | | ? { |
| 1514 | | ...lastGoodUsage, |
| 1515 | | ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 |
| 1516 | | ? { maxTokens } |
| 1517 | | : {}), |
| 1518 | | ...(typeof accumulatedTotalProcessedTokens === "number" && |
| 1519 | | Number.isFinite(accumulatedTotalProcessedTokens) && |
| 1520 | | accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens |
| 1521 | | ? { |
| 1522 | | totalProcessedTokens: accumulatedTotalProcessedTokens, |
| 1523 | | } |
| 1524 | | : {}), |
| 1525 | | } |
| 1526 | | : accumulatedSnapshot; |
| 1937 | const usageSnapshot: ThreadTokenUsageSnapshot | undefined = |
| 1938 | contextUsageSnapshot ?? |
| 1939 | (resultTotalOnly && lastGoodUsage |
| 1940 | ? { |
| 1941 | ...lastGoodUsage, |
| 1942 | ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 |
| 1943 | ? { maxTokens } |
| 1944 | : {}), |
| 1945 | ...(typeof accumulatedTotalProcessedTokens === "number" && |
| 1946 | Number.isFinite(accumulatedTotalProcessedTokens) && |
| 1947 | accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens |
| 1948 | ? { |
| 1949 | totalProcessedTokens: accumulatedTotalProcessedTokens, |
| 1950 | } |
| 1951 | : {}), |
| 1952 | } |
| 1953 | : resultIterationSnapshot) ?? |
| 1954 | (lastGoodUsage |
| 1955 | ? { |
| 1956 | ...lastGoodUsage, |
| 1957 | ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 |
| 1958 | ? { maxTokens } |
| 1959 | : {}), |
| 1960 | ...(typeof accumulatedTotalProcessedTokens === "number" && |
| 1961 | Number.isFinite(accumulatedTotalProcessedTokens) && |
| 1962 | accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens |
| 1963 | ? { |
| 1964 | totalProcessedTokens: accumulatedTotalProcessedTokens, |
| 1965 | } |
| 1966 | : {}), |
| 1967 | } |
| 1968 | : undefined); |
| 1527 | 1969 | |
| 1528 | 1970 | const turnState = context.turnState; |
| 1529 | 1971 | if (!turnState) { |
| 1530 | | if (usageSnapshot) { |
| 1531 | | const usageStamp = yield* makeEventStamp(); |
| 1532 | | yield* offerRuntimeEvent({ |
| 1533 | | type: "thread.token-usage.updated", |
| 1534 | | eventId: usageStamp.eventId, |
| 1535 | | provider: PROVIDER, |
| 1536 | | createdAt: usageStamp.createdAt, |
| 1537 | | threadId: context.session.threadId, |
| 1538 | | payload: { |
| 1539 | | usage: usageSnapshot, |
| 1540 | | }, |
| 1541 | | providerRefs: {}, |
| 1542 | | }); |
| 1543 | | } |
| 1972 | yield* emitThreadTokenUsage(context, usageSnapshot, { |
| 1973 | rawMethod: "claude/result", |
| 1974 | rawPayload: result ?? { status }, |
| 1975 | }); |
| 1544 | 1976 | |
| 1545 | 1977 | const stamp = yield* makeEventStamp(); |
| 1546 | 1978 | yield* offerRuntimeEvent({ |
| ... | ... | @@ -1611,21 +2043,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1611 | 2043 | items: [...turnState.items], |
| 1612 | 2044 | }); |
| 1613 | 2045 | |
| 1614 | | if (usageSnapshot) { |
| 1615 | | const usageStamp = yield* makeEventStamp(); |
| 1616 | | yield* offerRuntimeEvent({ |
| 1617 | | type: "thread.token-usage.updated", |
| 1618 | | eventId: usageStamp.eventId, |
| 1619 | | provider: PROVIDER, |
| 1620 | | createdAt: usageStamp.createdAt, |
| 1621 | | threadId: context.session.threadId, |
| 1622 | | turnId: turnState.turnId, |
| 1623 | | payload: { |
| 1624 | | usage: usageSnapshot, |
| 1625 | | }, |
| 1626 | | providerRefs: nativeProviderRefs(context), |
| 1627 | | }); |
| 1628 | | } |
| 2046 | yield* emitThreadTokenUsage(context, usageSnapshot, { |
| 2047 | rawMethod: "claude/result", |
| 2048 | rawPayload: result ?? { status }, |
| 2049 | }); |
| 1629 | 2050 | |
| 1630 | 2051 | const stamp = yield* makeEventStamp(); |
| 1631 | 2052 | yield* offerRuntimeEvent({ |
| ... | ... | @@ -1670,6 +2091,23 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1670 | 2091 | |
| 1671 | 2092 | const { event } = message; |
| 1672 | 2093 | |
| 2094 | if (event.type === "message_delta") { |
| 2095 | if (message.parent_tool_use_id !== null && message.parent_tool_use_id !== undefined) { |
| 2096 | return; |
| 2097 | } |
| 2098 | |
| 2099 | const snapshot = normalizeClaudeActiveTokenUsage( |
| 2100 | event.usage, |
| 2101 | context.lastKnownContextWindow, |
| 2102 | context.lastKnownTotalProcessedTokens, |
| 2103 | ); |
| 2104 | yield* emitThreadTokenUsage(context, snapshot, { |
| 2105 | rawMethod: "claude/stream_event/message_delta", |
| 2106 | rawPayload: message, |
| 2107 | }); |
| 2108 | return; |
| 2109 | } |
| 2110 | |
| 1673 | 2111 | if (event.type === "content_block_delta") { |
| 1674 | 2112 | if ( |
| 1675 | 2113 | (event.delta.type === "text_delta" || event.delta.type === "thinking_delta") && |
| ... | ... | @@ -1932,6 +2370,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1932 | 2370 | |
| 1933 | 2371 | const [index, tool] = toolEntry; |
| 1934 | 2372 | const itemStatus = toolResult.isError ? "failed" : "completed"; |
| 2373 | const toolUseResult = readClaudeToolUseResult(message); |
| 1935 | 2374 | const toolData = { |
| 1936 | 2375 | toolName: tool.toolName, |
| 1937 | 2376 | input: tool.input, |
| ... | ... | @@ -2016,6 +2455,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2016 | 2455 | }, |
| 2017 | 2456 | }); |
| 2018 | 2457 | |
| 2458 | if ( |
| 2459 | !toolResult.isError && |
| 2460 | applyClaudeTaskToolResult(context.claudeTasks, tool, toolUseResult) |
| 2461 | ) { |
| 2462 | yield* emitClaudeTaskPlanUpdated(context, { |
| 2463 | toolUseId: tool.itemId, |
| 2464 | rawMethod: "claude/user", |
| 2465 | rawPayload: message, |
| 2466 | }); |
| 2467 | } |
| 2468 | |
| 2019 | 2469 | context.inFlightTools.delete(index); |
| 2020 | 2470 | } |
| 2021 | 2471 | }); |
| ... | ... | @@ -2171,6 +2621,18 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2171 | 2621 | }); |
| 2172 | 2622 | return; |
| 2173 | 2623 | case "compact_boundary": |
| 2624 | yield* emitThreadTokenUsage( |
| 2625 | context, |
| 2626 | compactBoundaryTokenUsageSnapshot( |
| 2627 | message as unknown as Record<string, unknown>, |
| 2628 | context.lastKnownContextWindow, |
| 2629 | context.lastKnownTotalProcessedTokens, |
| 2630 | ), |
| 2631 | { |
| 2632 | rawMethod: "claude/system/compact_boundary", |
| 2633 | rawPayload: message, |
| 2634 | }, |
| 2635 | ); |
| 2174 | 2636 | yield* offerRuntimeEvent({ |
| 2175 | 2637 | ...base, |
| 2176 | 2638 | type: "thread.state.changed", |
| ... | ... | @@ -2229,25 +2691,14 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2229 | 2691 | }); |
| 2230 | 2692 | return; |
| 2231 | 2693 | case "task_progress": |
| 2232 | | if (message.usage) { |
| 2233 | | const normalizedUsage = normalizeClaudeTokenUsage( |
| 2234 | | message.usage, |
| 2235 | | context.lastKnownContextWindow, |
| 2236 | | ); |
| 2237 | | if (normalizedUsage) { |
| 2238 | | context.lastKnownTokenUsage = normalizedUsage; |
| 2239 | | const usageStamp = yield* makeEventStamp(); |
| 2240 | | yield* offerRuntimeEvent({ |
| 2241 | | ...base, |
| 2242 | | eventId: usageStamp.eventId, |
| 2243 | | createdAt: usageStamp.createdAt, |
| 2244 | | type: "thread.token-usage.updated", |
| 2245 | | payload: { |
| 2246 | | usage: normalizedUsage, |
| 2247 | | }, |
| 2248 | | }); |
| 2249 | | } |
| 2250 | | } |
| 2694 | yield* emitThreadTokenUsage( |
| 2695 | context, |
| 2696 | normalizeClaudeTaskProgressTokenUsage(message.usage, context), |
| 2697 | { |
| 2698 | rawMethod: "claude/system/task_progress", |
| 2699 | rawPayload: message, |
| 2700 | }, |
| 2701 | ); |
| 2251 | 2702 | yield* offerRuntimeEvent({ |
| 2252 | 2703 | ...base, |
| 2253 | 2704 | type: "task.progress", |
| ... | ... | @@ -2261,25 +2712,14 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2261 | 2712 | }); |
| 2262 | 2713 | return; |
| 2263 | 2714 | case "task_notification": |
| 2264 | | if (message.usage) { |
| 2265 | | const normalizedUsage = normalizeClaudeTokenUsage( |
| 2266 | | message.usage, |
| 2267 | | context.lastKnownContextWindow, |
| 2268 | | ); |
| 2269 | | if (normalizedUsage) { |
| 2270 | | context.lastKnownTokenUsage = normalizedUsage; |
| 2271 | | const usageStamp = yield* makeEventStamp(); |
| 2272 | | yield* offerRuntimeEvent({ |
| 2273 | | ...base, |
| 2274 | | eventId: usageStamp.eventId, |
| 2275 | | createdAt: usageStamp.createdAt, |
| 2276 | | type: "thread.token-usage.updated", |
| 2277 | | payload: { |
| 2278 | | usage: normalizedUsage, |
| 2279 | | }, |
| 2280 | | }); |
| 2281 | | } |
| 2282 | | } |
| 2715 | yield* emitThreadTokenUsage( |
| 2716 | context, |
| 2717 | normalizeClaudeTaskProgressTokenUsage(message.usage, context), |
| 2718 | { |
| 2719 | rawMethod: "claude/system/task_notification", |
| 2720 | rawPayload: message, |
| 2721 | }, |
| 2722 | ); |
| 2283 | 2723 | yield* offerRuntimeEvent({ |
| 2284 | 2724 | ...base, |
| 2285 | 2725 | type: "task.completed", |
| ... | ... | @@ -2669,6 +3109,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2669 | 3109 | const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>(); |
| 2670 | 3110 | const pendingUserInputs = new Map<ApprovalRequestId, PendingUserInput>(); |
| 2671 | 3111 | const inFlightTools = new Map<number, ToolInFlight>(); |
| 3112 | const claudeTasks = new Map<string, ClaudeTaskState>(); |
| 2672 | 3113 | |
| 2673 | 3114 | const contextRef = yield* Ref.make<ClaudeSessionContext | undefined>(undefined); |
| 2674 | 3115 | |
| ... | ... | @@ -2969,6 +3410,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2969 | 3410 | const caps = getClaudeModelCapabilities(modelSelection?.model); |
| 2970 | 3411 | const descriptors = getProviderOptionDescriptors({ caps }); |
| 2971 | 3412 | const apiModelId = modelSelection ? resolveClaudeApiModelId(modelSelection) : undefined; |
| 3413 | const initialContextWindow = selectedClaudeContextWindow(modelSelection); |
| 2972 | 3414 | const rawEffort = getModelSelectionStringOptionValue(modelSelection, "effort"); |
| 2973 | 3415 | const effort = resolveClaudeEffort(caps, rawEffort) ?? null; |
| 2974 | 3416 | const fastModeSupported = descriptors.some( |
| ... | ... | @@ -3094,9 +3536,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3094 | 3536 | pendingUserInputs, |
| 3095 | 3537 | turns: [], |
| 3096 | 3538 | inFlightTools, |
| 3539 | claudeTasks, |
| 3097 | 3540 | turnState: undefined, |
| 3098 | | lastKnownContextWindow: undefined, |
| 3541 | lastKnownContextWindow: initialContextWindow, |
| 3099 | 3542 | lastKnownTokenUsage: undefined, |
| 3543 | lastKnownTotalProcessedTokens: undefined, |
| 3100 | 3544 | lastAssistantUuid: resumeState?.resumeSessionAt, |
| 3101 | 3545 | lastThreadStartedId: undefined, |
| 3102 | 3546 | stopped: false, |