Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/server/src/textGeneration/OpenCode2TextGeneration.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { assert, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { ProviderInstanceId } from "@t3tools/contracts";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import { TestClock } from "effect/testing";

import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
import * as OpenCode2AdapterV2Testkit from "../orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts";
Expand Down Expand Up @@ -119,4 +122,70 @@ it.layer(layer)("OpenCode2TextGeneration", (it) => {
assert.deepEqual(failure.cause, refusal);
}).pipe(Effect.scoped),
);

it.effect("times out when the prompt request and its interrupt never answer", () =>
Effect.gen(function* () {
const [subscribe, connected, create, created] = OPENCODE2_TITLE_GENERATION;
const sessionId = "ses_f0ec85cf6ffebfalpvV2E9nocu";
const prompted = yield* Deferred.make<void>();
const server = yield* OpenCode2AdapterV2Testkit.replayServer(
{
provider: "opencode",
protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
version: "2.0.18",
scenario: "opencode2_title_generation_stalled",
entries: [
subscribe!,
connected!,
create!,
created!,
{
type: "expect_outbound",
frame: { type: "session.prompt", input: { sessionID: sessionId, text: "<any>" } },
},
{
type: "emit_inbound",
frame: { type: "sdk.response", operation: "session.prompt", data: "<hang>" },
},
{
type: "expect_outbound",
frame: { type: "session.interrupt", input: { sessionID: sessionId } },
},
{
type: "emit_inbound",
frame: { type: "sdk.response", operation: "session.interrupt", data: "<hang>" },
},
{
type: "expect_outbound",
frame: { type: "session.remove", input: { sessionID: sessionId } },
},
{
type: "emit_inbound",
frame: { type: "sdk.response", operation: "session.remove", data: null },
},
{ type: "runtime_exit", status: "success" },
],
},
{ onPrompt: () => Deferred.doneUnsafe(prompted, Effect.void) },
);
const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
Effect.provideService(OpenCode2Server.OpenCode2Server, server),
);
const generation = yield* textGeneration
.generateThreadTitle({
cwd: process.cwd(),
message: "fix the login redirect loop after oauth",
modelSelection: {
instanceId: ProviderInstanceId.make("opencode"),
model: "opencode/big-pickle",
},
})
.pipe(Effect.flip, Effect.forkScoped);
yield* Deferred.await(prompted);
yield* TestClock.adjust("3 minutes");
yield* TestClock.adjust("5 seconds");
const failure = yield* Fiber.join(generation);
assert.equal(failure.detail, "OpenCode did not finish generating in time.");
}).pipe(Effect.scoped),
);
});
65 changes: 37 additions & 28 deletions packages/provider-opencode/src/server/v2/textGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,38 +120,47 @@ const runOnServer = Effect.fn("Opencode2TextGeneration.runOnServer")(function* (
),
Effect.forkScoped,
);
const session = yield* client.session.create({
title: `T3 Code ${input.operation}`,
location: Location.PublicRef.make({ directory: AbsolutePath.make(input.cwd) }),
model: Model.Ref.make({
providerID: Provider.ID.make(parsed.providerID),
id: Model.ID.make(parsed.modelID),
...(variant === undefined ? {} : { variant: Model.VariantID.make(variant) }),
}),
permissions: GENERATION_PERMISSIONS,
});
sessionId = session.id;
yield* Effect.addFinalizer(() =>
client.session
.remove({ sessionID: session.id })
.pipe(Effect.timeout("5 seconds"), Effect.ignore({ log: true })),
);
const images = (input.attachments ?? []).flatMap((attachment) => {
if (attachment.type !== "image") return [];
const path = resolveAttachmentPath(attachment);
return path === null ? [] : [{ uri: `file://${path}`, name: attachment.name }];
});
yield* client.session.prompt({
sessionID: session.id,
text: input.prompt,
...(images.length === 0 ? {} : { files: images }),
// The limit covers creating the session and sending the prompt as well, since
// either request can stall while the event stream stays connected.
const generate = Effect.gen(function* () {
const session = yield* client.session.create({
title: `T3 Code ${input.operation}`,
location: Location.PublicRef.make({ directory: AbsolutePath.make(input.cwd) }),
model: Model.Ref.make({
providerID: Provider.ID.make(parsed.providerID),
id: Model.ID.make(parsed.modelID),
...(variant === undefined ? {} : { variant: Model.VariantID.make(variant) }),
}),
permissions: GENERATION_PERMISSIONS,
});
sessionId = session.id;
yield* Effect.addFinalizer(() =>
client.session
.remove({ sessionID: session.id })
.pipe(Effect.timeout("5 seconds"), Effect.ignore({ log: true })),
);
const images = (input.attachments ?? []).flatMap((attachment) => {
if (attachment.type !== "image") return [];
const path = resolveAttachmentPath(attachment);
return path === null ? [] : [{ uri: `file://${path}`, name: attachment.name }];
});
yield* client.session.prompt({
sessionID: session.id,
text: input.prompt,
...(images.length === 0 ? {} : { files: images }),
});
return yield* Deferred.await(outcome);
});
const result = yield* Deferred.await(outcome).pipe(
const result = yield* generate.pipe(
Effect.timeoutOrElse({
duration: GENERATION_TIMEOUT,
orElse: () =>
client.session.interrupt({ sessionID: Session.ID.make(session.id) }).pipe(
Effect.ignore,
(sessionId === undefined
? Effect.void
: client.session
.interrupt({ sessionID: Session.ID.make(sessionId) })
.pipe(Effect.timeout("5 seconds"), Effect.ignore)
).pipe(
Effect.as<Outcome>({
_tag: "failed",
detail: "OpenCode did not finish generating in time.",
Expand Down
Loading