| author | |
| committer | |
| log | bcd640bf4d81df20ac5686ff0ca86d861f909be2 |
| tree | 2cbd09cd447d00850caeff753a9cc0c35ced17a0 |
| parent | b49a2e677c6fba6d705ad16dbd913f56df4035d6 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>14 files changed, 90 insertions(+), 14 deletions(-)
apps/server/src/provider/Drivers/CursorDriver.ts+2| ... | ... | @@ -101,6 +101,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 101 | 101 | defaultConfig: (): CursorSettings => decodeCursorSettings({}), |
| 102 | 102 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 103 | 103 | Effect.gen(function* () { |
| 104 | const crypto = yield* Crypto.Crypto; | |
| 104 | 105 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 105 | 106 | const fileSystem = yield* FileSystem.FileSystem; |
| 106 | 107 | const path = yield* Path.Path; |
| ... | ... | @@ -133,6 +134,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 133 | 134 | |
| 134 | 135 | const checkProvider = checkCursorProviderStatus(effectiveConfig, processEnv).pipe( |
| 135 | 136 | Effect.map(stampIdentity), |
| 137 | Effect.provideService(Crypto.Crypto, crypto), | |
| 136 | 138 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 137 | 139 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 138 | 140 | Effect.provideService(Path.Path, path), |
apps/server/src/provider/Drivers/GrokDriver.ts+2| ... | ... | @@ -84,6 +84,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { |
| 84 | 84 | defaultConfig: (): GrokSettings => decodeGrokSettings({}), |
| 85 | 85 | create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => |
| 86 | 86 | Effect.gen(function* () { |
| 87 | const crypto = yield* Crypto.Crypto; | |
| 87 | 88 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 88 | 89 | const httpClient = yield* HttpClient.HttpClient; |
| 89 | 90 | const serverSettings = yield* ServerSettingsService; |
| ... | ... | @@ -114,6 +115,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { |
| 114 | 115 | |
| 115 | 116 | const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv).pipe( |
| 116 | 117 | Effect.map(stampIdentity), |
| 118 | Effect.provideService(Crypto.Crypto, crypto), | |
| 117 | 119 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| 118 | 120 | ); |
| 119 | 121 |
apps/server/src/provider/Layers/CursorAdapter.test.ts+5-2| ... | ... | @@ -228,7 +228,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { |
| 228 | 228 | assert.isDefined(delta); |
| 229 | 229 | if (delta?.type === "content.delta") { |
| 230 | 230 | assert.equal(delta.payload.delta, "hello from mock"); |
| 231 | assert.match(String(delta.itemId), /^assistant:mock-session-1:segment:0$/); | |
| 231 | assert.match(String(delta.itemId), /^assistant:mock-session-1:runtime:[^:]+:segment:0$/); | |
| 232 | 232 | } |
| 233 | 233 | |
| 234 | 234 | const assistantCompleted = runtimeEvents.find( |
| ... | ... | @@ -687,7 +687,10 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { |
| 687 | 687 | if (contentDelta?.type === "content.delta") { |
| 688 | 688 | assert.equal(String(contentDelta.turnId), String(turn.turnId)); |
| 689 | 689 | assert.equal(contentDelta.payload.delta, "hello from mock"); |
| 690 | assert.equal(String(contentDelta.itemId), "assistant:mock-session-1:segment:0"); | |
| 690 | assert.match( | |
| 691 | String(contentDelta.itemId), | |
| 692 | /^assistant:mock-session-1:runtime:[^:]+:segment:0$/, | |
| 693 | ); | |
| 691 | 694 | } |
| 692 | 695 | }); |
| 693 | 696 |
apps/server/src/provider/Layers/CursorAdapter.ts+1| ... | ... | @@ -558,6 +558,7 @@ export function makeCursorAdapter( |
| 558 | 558 | : {}), |
| 559 | 559 | ...acpNativeLoggers, |
| 560 | 560 | }).pipe( |
| 561 | Effect.provideService(Crypto.Crypto, crypto), | |
| 561 | 562 | Effect.provideService(Scope.Scope, sessionScope), |
| 562 | 563 | Effect.mapError( |
| 563 | 564 | (cause) => |
apps/server/src/provider/Layers/CursorProvider.test.ts+2-1| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import * as NodeOS from "node:os"; |
| 2 | 2 | |
| 3 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import type * as Crypto from "effect/Crypto"; | |
| 4 | 5 | import * as Effect from "effect/Effect"; |
| 5 | 6 | import * as FileSystem from "effect/FileSystem"; |
| 6 | 7 | import * as Path from "effect/Path"; |
| ... | ... | @@ -28,7 +29,7 @@ const runNode = <A, E>( |
| 28 | 29 | effect: Effect.Effect< |
| 29 | 30 | A, |
| 30 | 31 | E, |
| 31 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | |
| 32 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path | |
| 32 | 33 | >, |
| 33 | 34 | ): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))); |
| 34 | 35 |
apps/server/src/provider/Layers/CursorProvider.ts+2-1| ... | ... | @@ -11,6 +11,7 @@ import type { |
| 11 | 11 | import { ProviderDriverKind } from "@t3tools/contracts"; |
| 12 | 12 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 13 | 13 | import { causeErrorTag } from "@t3tools/shared/observability"; |
| 14 | import * as Crypto from "effect/Crypto"; | |
| 14 | 15 | import * as DateTime from "effect/DateTime"; |
| 15 | 16 | import * as Effect from "effect/Effect"; |
| 16 | 17 | import * as Exit from "effect/Exit"; |
| ... | ... | @@ -992,7 +993,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( |
| 992 | 993 | ): Effect.fn.Return< |
| 993 | 994 | ServerProviderDraft, |
| 994 | 995 | never, |
| 995 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | |
| 996 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path | |
| 996 | 997 | > { |
| 997 | 998 | const checkedAt = DateTime.formatIso(yield* DateTime.now); |
| 998 | 999 | const fallbackModels = getCursorFallbackModels(cursorSettings); |
apps/server/src/provider/Layers/GrokAdapter.ts+1| ... | ... | @@ -596,6 +596,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte |
| 596 | 596 | : {}), |
| 597 | 597 | ...acpNativeLoggers, |
| 598 | 598 | }).pipe( |
| 599 | Effect.provideService(Crypto.Crypto, crypto), | |
| 599 | 600 | Effect.provideService(Scope.Scope, sessionScope), |
| 600 | 601 | Effect.mapError( |
| 601 | 602 | (cause) => |
apps/server/src/provider/Layers/GrokProvider.ts+6-1| ... | ... | @@ -7,6 +7,7 @@ import { |
| 7 | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 9 | 9 | import { causeErrorTag } from "@t3tools/shared/observability"; |
| 10 | import * as Crypto from "effect/Crypto"; | |
| 10 | 11 | import * as DateTime from "effect/DateTime"; |
| 11 | 12 | import * as Effect from "effect/Effect"; |
| 12 | 13 | import * as Exit from "effect/Exit"; |
| ... | ... | @@ -167,7 +168,11 @@ const runGrokVersionCommand = ( |
| 167 | 168 | export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(function* ( |
| 168 | 169 | grokSettings: GrokSettings, |
| 169 | 170 | environment: NodeJS.ProcessEnv = process.env, |
| 170 | ): Effect.fn.Return<ServerProviderDraft, never, ChildProcessSpawner.ChildProcessSpawner> { | |
| 171 | ): Effect.fn.Return< | |
| 172 | ServerProviderDraft, | |
| 173 | never, | |
| 174 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | |
| 175 | > { | |
| 171 | 176 | const checkedAt = DateTime.formatIso(yield* DateTime.now); |
| 172 | 177 | const fallbackModels = grokModelsFromSettings(grokSettings.customModels); |
| 173 | 178 |
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+37| ... | ... | @@ -116,6 +116,43 @@ describe("AcpSessionRuntime", () => { |
| 116 | 116 | ), |
| 117 | 117 | ); |
| 118 | 118 | |
| 119 | it.effect("keeps assistant item IDs unique when a provider session restarts", () => { | |
| 120 | const collectFirstAssistantItemId = Effect.gen(function* () { | |
| 121 | const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; | |
| 122 | const started = yield* runtime.start(); | |
| 123 | expect(started.sessionId).toBe("mock-session-1"); | |
| 124 | ||
| 125 | yield* runtime.prompt({ | |
| 126 | prompt: [{ type: "text", text: "hi" }], | |
| 127 | }); | |
| 128 | ||
| 129 | const events = Array.from(yield* Stream.runCollect(Stream.take(runtime.getEvents(), 4))); | |
| 130 | const assistantStart = events.find((event) => event._tag === "AssistantItemStarted"); | |
| 131 | expect(assistantStart?._tag).toBe("AssistantItemStarted"); | |
| 132 | return assistantStart?._tag === "AssistantItemStarted" ? assistantStart.itemId : ""; | |
| 133 | }).pipe( | |
| 134 | Effect.provide( | |
| 135 | AcpSessionRuntime.layer({ | |
| 136 | spawn: { | |
| 137 | command: mockAgentCommand, | |
| 138 | args: mockAgentArgs, | |
| 139 | }, | |
| 140 | cwd: process.cwd(), | |
| 141 | clientInfo: { name: "t3-test", version: "0.0.0" }, | |
| 142 | authMethodId: "test", | |
| 143 | }), | |
| 144 | ), | |
| 145 | Effect.scoped, | |
| 146 | ); | |
| 147 | ||
| 148 | return Effect.gen(function* () { | |
| 149 | const beforeRestart = yield* collectFirstAssistantItemId; | |
| 150 | const afterRestart = yield* collectFirstAssistantItemId; | |
| 151 | ||
| 152 | expect(afterRestart).not.toBe(beforeRestart); | |
| 153 | }).pipe(Effect.provide(NodeServices.layer)); | |
| 154 | }); | |
| 155 | ||
| 119 | 156 | it.effect("drops session updates emitted for a child ACP session", () => |
| 120 | 157 | Effect.gen(function* () { |
| 121 | 158 | const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; |
apps/server/src/provider/acp/AcpSessionRuntime.ts+22-5| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import * as Cause from "effect/Cause"; |
| 2 | 2 | import * as Clock from "effect/Clock"; |
| 3 | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | |
| 4 | 5 | import * as Deferred from "effect/Deferred"; |
| 5 | 6 | import * as Duration from "effect/Duration"; |
| 6 | 7 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -270,14 +271,24 @@ export const make = ( |
| 270 | 271 | ): Effect.Effect< |
| 271 | 272 | AcpSessionRuntime["Service"], |
| 272 | 273 | EffectAcpErrors.AcpError, |
| 273 | ChildProcessSpawner.ChildProcessSpawner | Scope.Scope | |
| 274 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope | |
| 274 | 275 | > => |
| 275 | 276 | Effect.gen(function* () { |
| 277 | const crypto = yield* Crypto.Crypto; | |
| 276 | 278 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 277 | 279 | const runtimeScope = yield* Scope.Scope; |
| 278 | 280 | const eventQueue = yield* Queue.unbounded<AcpSessionRuntimeEvent>(); |
| 279 | 281 | const modeStateRef = yield* Ref.make<AcpSessionModeState | undefined>(undefined); |
| 280 | 282 | const toolCallsRef = yield* Ref.make(new Map<string, AcpToolCallState>()); |
| 283 | const assistantItemRuntimeId = yield* crypto.randomUUIDv4.pipe( | |
| 284 | Effect.mapError( | |
| 285 | (cause) => | |
| 286 | new EffectAcpErrors.AcpTransportError({ | |
| 287 | detail: "Failed to generate an ACP assistant item runtime identifier.", | |
| 288 | cause, | |
| 289 | }), | |
| 290 | ), | |
| 291 | ); | |
| 281 | 292 | const assistantSegmentRef = yield* Ref.make<AcpAssistantSegmentState>({ nextSegmentIndex: 0 }); |
| 282 | 293 | const configOptionsRef = yield* Ref.make(sessionConfigOptionsFromSetup(undefined)); |
| 283 | 294 | const startStateRef = yield* Ref.make<AcpStartState>({ _tag: "NotStarted" }); |
| ... | ... | @@ -387,6 +398,7 @@ export const make = ( |
| 387 | 398 | modeStateRef, |
| 388 | 399 | toolCallsRef, |
| 389 | 400 | assistantSegmentRef, |
| 401 | assistantItemRuntimeId, | |
| 390 | 402 | params: notification, |
| 391 | 403 | }); |
| 392 | 404 | }), |
| ... | ... | @@ -802,7 +814,7 @@ export const layer = ( |
| 802 | 814 | ): Layer.Layer< |
| 803 | 815 | AcpSessionRuntime, |
| 804 | 816 | EffectAcpErrors.AcpError, |
| 805 | ChildProcessSpawner.ChildProcessSpawner | |
| 817 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | |
| 806 | 818 | > => Layer.effect(AcpSessionRuntime, make(options)); |
| 807 | 819 | |
| 808 | 820 | function sessionConfigOptionsFromSetup( |
| ... | ... | @@ -834,12 +846,14 @@ const handleSessionUpdate = ({ |
| 834 | 846 | modeStateRef, |
| 835 | 847 | toolCallsRef, |
| 836 | 848 | assistantSegmentRef, |
| 849 | assistantItemRuntimeId, | |
| 837 | 850 | params, |
| 838 | 851 | }: { |
| 839 | 852 | readonly queue: Queue.Queue<AcpSessionRuntimeEvent>; |
| 840 | 853 | readonly modeStateRef: Ref.Ref<AcpSessionModeState | undefined>; |
| 841 | 854 | readonly toolCallsRef: Ref.Ref<Map<string, AcpToolCallState>>; |
| 842 | 855 | readonly assistantSegmentRef: Ref.Ref<AcpAssistantSegmentState>; |
| 856 | readonly assistantItemRuntimeId: string; | |
| 843 | 857 | readonly params: EffectAcpSchema.SessionNotification; |
| 844 | 858 | }): Effect.Effect<void> => |
| 845 | 859 | Effect.gen(function* () { |
| ... | ... | @@ -887,6 +901,7 @@ const handleSessionUpdate = ({ |
| 887 | 901 | queue, |
| 888 | 902 | assistantSegmentRef, |
| 889 | 903 | sessionId: params.sessionId, |
| 904 | assistantItemRuntimeId, | |
| 890 | 905 | }); |
| 891 | 906 | yield* Queue.offer(queue, { |
| 892 | 907 | ...event, |
| ... | ... | @@ -924,17 +939,19 @@ function shouldEmitToolCallUpdate( |
| 924 | 939 | return previous === undefined || previous.title !== next.title || previous.detail !== next.detail; |
| 925 | 940 | } |
| 926 | 941 | |
| 927 | const assistantItemId = (sessionId: string, segmentIndex: number) => | |
| 928 | `assistant:${sessionId}:segment:${segmentIndex}`; | |
| 942 | const assistantItemId = (sessionId: string, runtimeId: string, segmentIndex: number) => | |
| 943 | `assistant:${sessionId}:runtime:${runtimeId}:segment:${segmentIndex}`; | |
| 929 | 944 | |
| 930 | 945 | const ensureActiveAssistantSegment = ({ |
| 931 | 946 | queue, |
| 932 | 947 | assistantSegmentRef, |
| 933 | 948 | sessionId, |
| 949 | assistantItemRuntimeId, | |
| 934 | 950 | }: { |
| 935 | 951 | readonly queue: Queue.Queue<AcpSessionRuntimeEvent>; |
| 936 | 952 | readonly assistantSegmentRef: Ref.Ref<AcpAssistantSegmentState>; |
| 937 | 953 | readonly sessionId: string; |
| 954 | readonly assistantItemRuntimeId: string; | |
| 938 | 955 | }) => |
| 939 | 956 | Ref.modify<AcpAssistantSegmentState, EnsureActiveAssistantSegmentResult>( |
| 940 | 957 | assistantSegmentRef, |
| ... | ... | @@ -942,7 +959,7 @@ const ensureActiveAssistantSegment = ({ |
| 942 | 959 | if (current.activeItemId) { |
| 943 | 960 | return [{ itemId: current.activeItemId }, current] as const; |
| 944 | 961 | } |
| 945 | const itemId = assistantItemId(sessionId, current.nextSegmentIndex); | |
| 962 | const itemId = assistantItemId(sessionId, assistantItemRuntimeId, current.nextSegmentIndex); | |
| 946 | 963 | return [ |
| 947 | 964 | { |
| 948 | 965 | itemId, |
apps/server/src/provider/acp/CursorAcpSupport.ts+2-1| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import { type CursorSettings, type ProviderOptionSelection } from "@t3tools/contracts"; |
| 2 | import * as Crypto from "effect/Crypto"; | |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Layer from "effect/Layer"; |
| 4 | 5 | import * as Scope from "effect/Scope"; |
| ... | ... | @@ -50,7 +51,7 @@ export const makeCursorAcpRuntime = ( |
| 50 | 51 | ): Effect.Effect< |
| 51 | 52 | AcpSessionRuntime.AcpSessionRuntime["Service"], |
| 52 | 53 | EffectAcpErrors.AcpError, |
| 53 | Scope.Scope | |
| 54 | Crypto.Crypto | Scope.Scope | |
| 54 | 55 | > => |
| 55 | 56 | Effect.gen(function* () { |
| 56 | 57 | const acpContext = yield* Layer.build( |
apps/server/src/provider/acp/GrokAcpSupport.ts+2-1| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import { type GrokSettings, ProviderDriverKind } from "@t3tools/contracts"; |
| 2 | import * as Crypto from "effect/Crypto"; | |
| 2 | 3 | import * as Effect from "effect/Effect"; |
| 3 | 4 | import * as Layer from "effect/Layer"; |
| 4 | 5 | import * as Scope from "effect/Scope"; |
| ... | ... | @@ -55,7 +56,7 @@ export const makeGrokAcpRuntime = ( |
| 55 | 56 | ): Effect.Effect< |
| 56 | 57 | AcpSessionRuntime.AcpSessionRuntime["Service"], |
| 57 | 58 | EffectAcpErrors.AcpError, |
| 58 | Scope.Scope | |
| 59 | Crypto.Crypto | Scope.Scope | |
| 59 | 60 | > => |
| 60 | 61 | Effect.gen(function* () { |
| 61 | 62 | const acpContext = yield* Layer.build( |
apps/server/src/textGeneration/CursorTextGeneration.ts+3-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as Crypto from "effect/Crypto"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Option from "effect/Option"; |
| 3 | 4 | import * as Ref from "effect/Ref"; |
| ... | ... | @@ -38,6 +39,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu |
| 38 | 39 | cursorSettings: CursorSettings, |
| 39 | 40 | environment?: NodeJS.ProcessEnv, |
| 40 | 41 | ) { |
| 42 | const crypto = yield* Crypto.Crypto; | |
| 41 | 43 | const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 42 | 44 | const resolvedEnvironment = environment ?? process.env; |
| 43 | 45 | |
| ... | ... | @@ -66,7 +68,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu |
| 66 | 68 | childProcessSpawner: commandSpawner, |
| 67 | 69 | cwd, |
| 68 | 70 | clientInfo: { name: "t3-code-git-text", version: "0.0.0" }, |
| 69 | }); | |
| 71 | }).pipe(Effect.provideService(Crypto.Crypto, crypto)); | |
| 70 | 72 | |
| 71 | 73 | yield* runtime.handleSessionUpdate((notification) => { |
| 72 | 74 | const update = notification.update; |
apps/server/src/textGeneration/GrokTextGeneration.ts+3-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as Crypto from "effect/Crypto"; | |
| 1 | 2 | import * as Effect from "effect/Effect"; |
| 2 | 3 | import * as Option from "effect/Option"; |
| 3 | 4 | import * as Ref from "effect/Ref"; |
| ... | ... | @@ -37,6 +38,7 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi |
| 37 | 38 | grokSettings: GrokSettings, |
| 38 | 39 | environment: NodeJS.ProcessEnv = process.env, |
| 39 | 40 | ) { |
| 41 | const crypto = yield* Crypto.Crypto; | |
| 40 | 42 | const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 41 | 43 | |
| 42 | 44 | const runGrokJson = <S extends Schema.Top>({ |
| ... | ... | @@ -65,7 +67,7 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi |
| 65 | 67 | childProcessSpawner: commandSpawner, |
| 66 | 68 | cwd, |
| 67 | 69 | clientInfo: { name: "t3-code-git-text", version: "0.0.0" }, |
| 68 | }); | |
| 70 | }).pipe(Effect.provideService(Crypto.Crypto, crypto)); | |
| 69 | 71 | |
| 70 | 72 | yield* runtime.handleSessionUpdate((notification) => { |
| 71 | 73 | const update = notification.update; |