Skip to content

Commit 61163cb

Browse files
Honakitlangton
andauthored
fix(server): let agent and provider lists wait for plugins (#59082)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
1 parent f699163 commit 61163cb

6 files changed

Lines changed: 39 additions & 12 deletions

File tree

‎packages/client/src/promise/generated/client.ts‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/protocol/openapi.json‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/protocol/src/groups/agent.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { Location } from "@opencode/schema/location"
33
import { Schema } from "effect"
44
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api"
55
import { LocationQuery, locationQueryOpenApi } from "./location.js"
6-
import { AgentNotFoundError } from "../errors.js"
6+
import { AgentNotFoundError, ServiceUnavailableError } from "../errors.js"
77

88
export const AgentGroup = HttpApiGroup.make("server.agent")
99
.add(
1010
HttpApiEndpoint.get("agent.list", "/api/agent", {
1111
query: LocationQuery,
1212
success: Location.response(Schema.Array(Agent.Info)),
13+
error: ServiceUnavailableError,
1314
})
1415
.annotateMerge(locationQueryOpenApi)
1516
.annotateMerge(

‎packages/server/src/handlers/agent.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { Agent } from "@opencode/core/agent"
22
import { Effect } from "effect"
33
import { HttpApiBuilder } from "effect/http-api"
44
import { Api } from "../api"
5-
import { response } from "../location"
5+
import { awaitPlugins, response } from "../location"
66
import { AgentNotFoundError } from "@opencode/protocol/errors"
77

88
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
99
handlers
1010
.handle("agent.list", () =>
1111
Effect.gen(function* () {
12+
yield* awaitPlugins
1213
return yield* response(Agent.Service.use((agent) => agent.list()))
1314
}),
1415
)

‎packages/server/src/handlers/provider.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { ProviderNotFoundError } from "@opencode/protocol/errors"
33
import { Effect } from "effect"
44
import { HttpApiBuilder } from "effect/http-api"
55
import { Api } from "../api"
6-
import { response } from "../location"
6+
import { awaitPlugins, response } from "../location"
77

88
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
99
Effect.gen(function* () {
1010
return handlers
1111
.handle(
1212
"provider.list",
1313
Effect.fn(function* () {
14+
yield* awaitPlugins
1415
const providers = yield* Provider.Service
1516
return yield* response(providers.available())
1617
}),

‎packages/server/test/provider.test.ts‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { it } from "../../core/test/lib/effect"
88
import { createRoutes } from "../src/routes"
99

1010
it.live(
11-
"provider reads stay nonblocking while authentication discovery waits for plugins",
11+
"agent and provider lists wait for plugins while single provider reads stay nonblocking",
1212
() =>
1313
Effect.gen(function* () {
1414
const tmp = yield* tmpdirScoped("opencode-provider-endpoints-")
@@ -32,6 +32,7 @@ it.live(
3232
models: { chat: {} },
3333
},
3434
},
35+
agents: { reviewer: { description: "Reviews changes" } },
3536
}),
3637
},
3738
}).pipe(Layer.provide(HttpServer.layerServices)),
@@ -62,22 +63,35 @@ it.live(
6263
}),
6364
)
6465
})
65-
// Config providers activate after SDK plugins; reads must return the current snapshot without waiting.
66-
const list = yield* request("GET", "/api/provider").pipe(Effect.timeout("2 seconds"))
66+
// Config providers and agents activate after SDK plugins, so the lists wait rather than answer with a partial
67+
// catalog that a client would cache as loaded.
68+
const providers = yield* request("GET", "/api/provider").pipe(Effect.forkScoped)
69+
const agents = yield* request("GET", "/api/agent").pipe(Effect.forkScoped)
6770
yield* Deferred.await(started)
6871
const integrations = yield* request("GET", "/api/integration").pipe(Effect.forkScoped)
69-
expect(list.status).toBe(200)
70-
expect(yield* Effect.promise(() => list.json())).toMatchObject({
71-
location: { directory: tmp.path },
72-
data: expect.not.arrayContaining([expect.objectContaining({ id: "custom" })]),
73-
})
7472
const get = yield* request("GET", "/api/provider/custom").pipe(Effect.timeout("2 seconds"))
7573
expect(get.status).toBe(404)
7674
expect(yield* Effect.promise(() => get.json())).toMatchObject({
7775
_tag: "ProviderNotFoundError",
7876
providerID: "custom",
7977
})
78+
expect(providers.pollUnsafe()).toBeUndefined()
79+
expect(agents.pollUnsafe()).toBeUndefined()
8080
yield* Deferred.succeed(release, undefined)
81+
const list = yield* Fiber.join(providers)
82+
expect(list.status).toBe(200)
83+
expect(yield* Effect.promise(() => list.json())).toMatchObject({
84+
location: { directory: tmp.path },
85+
data: expect.arrayContaining([expect.objectContaining({ id: "custom" })]),
86+
})
87+
const agentList = yield* Fiber.join(agents)
88+
expect(agentList.status).toBe(200)
89+
expect(yield* Effect.promise(() => agentList.json())).toMatchObject({
90+
data: expect.arrayContaining([
91+
expect.objectContaining({ id: "build" }),
92+
expect.objectContaining({ id: "reviewer" }),
93+
]),
94+
})
8195
const discovered = yield* Fiber.join(integrations)
8296
expect(discovered.status).toBe(200)
8397
expect(yield* Effect.promise(() => discovered.json())).toMatchObject({

0 commit comments

Comments
 (0)