| ... | ... | @@ -1791,92 +1791,209 @@ describe("ClaudeAdapterLive", () => { |
| 1791 | 1791 | ); |
| 1792 | 1792 | }); |
| 1793 | 1793 | |
| 1794 | | it.effect("keeps a resumed replacement session during slow stop cleanup", () => { |
| 1795 | | const queries: FakeClaudeQuery[] = []; |
| 1796 | | let signalUsageStarted: () => void = () => undefined; |
| 1797 | | const usageStarted = new Promise<void>((resolve) => { |
| 1798 | | signalUsageStarted = resolve; |
| 1794 | it.effect("completes with result usage without querying current context usage", () => { |
| 1795 | const harness = makeHarness(); |
| 1796 | let getContextUsageCalls = 0; |
| 1797 | Object.assign(harness.query, { |
| 1798 | getContextUsage: async () => { |
| 1799 | getContextUsageCalls += 1; |
| 1800 | return { |
| 1801 | totalTokens: 999, |
| 1802 | maxTokens: 200000, |
| 1803 | isAutoCompactEnabled: true, |
| 1804 | }; |
| 1805 | }, |
| 1799 | 1806 | }); |
| 1800 | | const layer = Layer.effect( |
| 1801 | | ClaudeAdapter, |
| 1802 | | Effect.gen(function* () { |
| 1803 | | const claudeConfig = decodeClaudeSettings({}); |
| 1804 | | return yield* makeClaudeAdapter(claudeConfig, { |
| 1805 | | createQuery: () => { |
| 1806 | | const query = new FakeClaudeQuery(); |
| 1807 | | if (queries.length === 0) { |
| 1808 | | Object.assign(query, { |
| 1809 | | getContextUsage: async () => { |
| 1810 | | signalUsageStarted(); |
| 1811 | | return await new Promise<never>(() => undefined); |
| 1812 | | }, |
| 1813 | | }); |
| 1814 | | } |
| 1815 | | queries.push(query); |
| 1816 | | return query; |
| 1817 | | }, |
| 1818 | | }); |
| 1819 | | }), |
| 1820 | | ).pipe( |
| 1821 | | Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")), |
| 1822 | | Layer.provideMerge(ServerSettingsService.layerTest()), |
| 1823 | | Layer.provideMerge(NodeServices.layer), |
| 1824 | | ); |
| 1825 | | |
| 1826 | 1807 | return Effect.gen(function* () { |
| 1827 | 1808 | const adapter = yield* ClaudeAdapter; |
| 1828 | | const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe( |
| 1809 | const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe( |
| 1829 | 1810 | Stream.runCollect, |
| 1830 | 1811 | Effect.forkChild, |
| 1831 | 1812 | ); |
| 1832 | | const firstSession = yield* adapter.startSession({ |
| 1813 | yield* adapter.startSession({ |
| 1833 | 1814 | threadId: THREAD_ID, |
| 1834 | 1815 | provider: ProviderDriverKind.make("claudeAgent"), |
| 1835 | 1816 | runtimeMode: "full-access", |
| 1836 | 1817 | }); |
| 1837 | 1818 | yield* adapter.sendTurn({ |
| 1838 | | threadId: firstSession.threadId, |
| 1819 | threadId: THREAD_ID, |
| 1839 | 1820 | input: "hello", |
| 1840 | 1821 | attachments: [], |
| 1841 | 1822 | }); |
| 1842 | 1823 | |
| 1843 | | const interruptFiber = yield* adapter |
| 1844 | | .interruptTurn(firstSession.threadId) |
| 1845 | | .pipe(Effect.forkChild); |
| 1846 | | yield* Effect.promise(() => usageStarted); |
| 1847 | | assert.equal(queries[0]?.closeCalls, 1); |
| 1824 | harness.query.emit({ |
| 1825 | type: "assistant", |
| 1826 | session_id: "sdk-session-result-usage", |
| 1827 | uuid: "assistant-result-usage-1", |
| 1828 | parent_tool_use_id: null, |
| 1829 | message: { |
| 1830 | id: "assistant-message-result-usage-1", |
| 1831 | role: "assistant", |
| 1832 | content: [], |
| 1833 | usage: { |
| 1834 | input_tokens: 80, |
| 1835 | output_tokens: 20, |
| 1836 | }, |
| 1837 | }, |
| 1838 | } as unknown as SDKMessage); |
| 1839 | harness.query.emit({ |
| 1840 | type: "assistant", |
| 1841 | session_id: "sdk-session-result-usage", |
| 1842 | uuid: "assistant-result-usage-2", |
| 1843 | parent_tool_use_id: null, |
| 1844 | message: { |
| 1845 | id: "assistant-message-result-usage-2", |
| 1846 | role: "assistant", |
| 1847 | content: [], |
| 1848 | usage: { |
| 1849 | input_tokens: 180, |
| 1850 | output_tokens: 20, |
| 1851 | }, |
| 1852 | }, |
| 1853 | } as unknown as SDKMessage); |
| 1854 | harness.query.emit({ |
| 1855 | type: "assistant", |
| 1856 | session_id: "sdk-session-result-usage", |
| 1857 | uuid: "assistant-result-usage-3", |
| 1858 | parent_tool_use_id: null, |
| 1859 | message: { |
| 1860 | id: "assistant-message-result-usage-3", |
| 1861 | role: "assistant", |
| 1862 | content: [], |
| 1863 | }, |
| 1864 | } as unknown as SDKMessage); |
| 1865 | harness.query.emit({ |
| 1866 | type: "result", |
| 1867 | subtype: "success", |
| 1868 | is_error: false, |
| 1869 | duration_ms: 1234, |
| 1870 | duration_api_ms: 1200, |
| 1871 | num_turns: 1, |
| 1872 | result: "done", |
| 1873 | stop_reason: "end_turn", |
| 1874 | session_id: "sdk-session-result-usage", |
| 1875 | usage: { |
| 1876 | input_tokens: 400, |
| 1877 | output_tokens: 50, |
| 1878 | }, |
| 1879 | modelUsage: { |
| 1880 | "claude-opus-4-6": { |
| 1881 | contextWindow: 200000, |
| 1882 | maxOutputTokens: 64000, |
| 1883 | }, |
| 1884 | }, |
| 1885 | } as unknown as SDKMessage); |
| 1848 | 1886 | |
| 1849 | | const replacement = yield* adapter.startSession({ |
| 1887 | const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); |
| 1888 | assert.equal(getContextUsageCalls, 0); |
| 1889 | const usageEvent = runtimeEvents.find((event) => event.type === "thread.token-usage.updated"); |
| 1890 | assert.equal(usageEvent?.type, "thread.token-usage.updated"); |
| 1891 | if (usageEvent?.type === "thread.token-usage.updated") { |
| 1892 | assert.deepEqual(usageEvent.payload.usage, { |
| 1893 | usedTokens: 200, |
| 1894 | lastUsedTokens: 200, |
| 1895 | totalProcessedTokens: 450, |
| 1896 | inputTokens: 180, |
| 1897 | outputTokens: 20, |
| 1898 | maxTokens: 200000, |
| 1899 | }); |
| 1900 | } |
| 1901 | assert.equal( |
| 1902 | runtimeEvents.find((event) => event.type === "turn.completed")?.type, |
| 1903 | "turn.completed", |
| 1904 | ); |
| 1905 | }).pipe( |
| 1906 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 1907 | Effect.provide(harness.layer), |
| 1908 | ); |
| 1909 | }); |
| 1910 | |
| 1911 | it.effect("preserves compacted usage when completion follows an older assistant frame", () => { |
| 1912 | const harness = makeHarness(); |
| 1913 | return Effect.gen(function* () { |
| 1914 | const adapter = yield* ClaudeAdapter; |
| 1915 | const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( |
| 1916 | Stream.runCollect, |
| 1917 | Effect.forkChild, |
| 1918 | ); |
| 1919 | |
| 1920 | yield* adapter.startSession({ |
| 1850 | 1921 | threadId: THREAD_ID, |
| 1851 | 1922 | provider: ProviderDriverKind.make("claudeAgent"), |
| 1852 | 1923 | runtimeMode: "full-access", |
| 1853 | | resumeCursor: firstSession.resumeCursor, |
| 1854 | 1924 | }); |
| 1855 | | yield* TestClock.adjust("1 second"); |
| 1856 | | yield* Fiber.join(interruptFiber); |
| 1925 | yield* adapter.sendTurn({ |
| 1926 | threadId: THREAD_ID, |
| 1927 | input: "hello", |
| 1928 | attachments: [], |
| 1929 | }); |
| 1930 | harness.query.emit({ |
| 1931 | type: "assistant", |
| 1932 | session_id: "sdk-session-compacted-usage", |
| 1933 | uuid: "assistant-compacted-usage", |
| 1934 | parent_tool_use_id: null, |
| 1935 | message: { |
| 1936 | id: "assistant-message-compacted-usage", |
| 1937 | role: "assistant", |
| 1938 | content: [], |
| 1939 | usage: { |
| 1940 | input_tokens: 180, |
| 1941 | output_tokens: 20, |
| 1942 | }, |
| 1943 | }, |
| 1944 | } as unknown as SDKMessage); |
| 1945 | harness.query.emit({ |
| 1946 | type: "system", |
| 1947 | subtype: "compact_boundary", |
| 1948 | compact_metadata: { |
| 1949 | pre_tokens: 200, |
| 1950 | post_tokens: 40, |
| 1951 | }, |
| 1952 | session_id: "sdk-session-compacted-usage", |
| 1953 | uuid: "compact-boundary-usage", |
| 1954 | } as unknown as SDKMessage); |
| 1955 | harness.query.emit({ |
| 1956 | type: "result", |
| 1957 | subtype: "success", |
| 1958 | is_error: false, |
| 1959 | duration_ms: 1234, |
| 1960 | duration_api_ms: 1200, |
| 1961 | num_turns: 2, |
| 1962 | result: "done", |
| 1963 | stop_reason: "end_turn", |
| 1964 | session_id: "sdk-session-compacted-usage", |
| 1965 | usage: { |
| 1966 | input_tokens: 400, |
| 1967 | output_tokens: 50, |
| 1968 | }, |
| 1969 | modelUsage: { |
| 1970 | "claude-opus-4-6": { |
| 1971 | contextWindow: 200000, |
| 1972 | maxOutputTokens: 64000, |
| 1973 | }, |
| 1974 | }, |
| 1975 | } as unknown as SDKMessage); |
| 1857 | 1976 | |
| 1858 | | const activeSessions = yield* adapter.listSessions(); |
| 1859 | 1977 | const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); |
| 1860 | | assert.equal(queries.length, 2); |
| 1861 | | assert.equal(queries[1]?.closeCalls, 0); |
| 1862 | | assert.equal(activeSessions.length, 1); |
| 1863 | | assert.deepEqual(activeSessions[0]?.resumeCursor, replacement.resumeCursor); |
| 1864 | | assert.deepEqual( |
| 1865 | | runtimeEvents |
| 1866 | | .filter((event) => event.type.startsWith("session.")) |
| 1867 | | .map((event) => event.type), |
| 1868 | | [ |
| 1869 | | "session.started", |
| 1870 | | "session.configured", |
| 1871 | | "session.state.changed", |
| 1872 | | "session.started", |
| 1873 | | "session.configured", |
| 1874 | | "session.state.changed", |
| 1875 | | ], |
| 1978 | const finalUsageEvent = runtimeEvents.findLast( |
| 1979 | (event) => event.type === "thread.token-usage.updated", |
| 1980 | ); |
| 1981 | assert.equal(finalUsageEvent?.type, "thread.token-usage.updated"); |
| 1982 | if (finalUsageEvent?.type === "thread.token-usage.updated") { |
| 1983 | assert.deepEqual(finalUsageEvent.payload.usage, { |
| 1984 | usedTokens: 40, |
| 1985 | lastUsedTokens: 200, |
| 1986 | totalProcessedTokens: 450, |
| 1987 | maxTokens: 200000, |
| 1988 | }); |
| 1989 | } |
| 1990 | assert.equal( |
| 1991 | runtimeEvents.find((event) => event.type === "turn.completed")?.type, |
| 1992 | "turn.completed", |
| 1876 | 1993 | ); |
| 1877 | 1994 | }).pipe( |
| 1878 | 1995 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 1879 | | Effect.provide(layer), |
| 1996 | Effect.provide(harness.layer), |
| 1880 | 1997 | ); |
| 1881 | 1998 | }); |
| 1882 | 1999 | |