|
|
@@ -56,6 +56,7 @@ import { reply, TestLLMServer } from "../lib/llm-server"
|
|
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
|
import { ModelV2 } from "@opencode-ai/core/model"
|
|
|
+import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
|
|
|
|
|
const summary = Layer.succeed(
|
|
|
SessionSummary.Service,
|
|
|
@@ -108,29 +109,32 @@ function errorTool(parts: SessionV1.Part[]) {
|
|
|
return part?.state.status === "error" ? (part as ErrorToolPart) : undefined
|
|
|
}
|
|
|
|
|
|
-const mcp = Layer.succeed(
|
|
|
- MCP.Service,
|
|
|
- MCP.Service.of({
|
|
|
- status: () => Effect.succeed({}),
|
|
|
- clients: () => Effect.succeed({}),
|
|
|
- tools: () => Effect.succeed({}),
|
|
|
- prompts: () => Effect.succeed({}),
|
|
|
- resources: () => Effect.succeed({}),
|
|
|
- resourceTemplates: () => Effect.succeed({}),
|
|
|
- add: () => Effect.succeed({ status: { status: "disabled" as const } }),
|
|
|
- connect: () => Effect.void,
|
|
|
- disconnect: () => Effect.void,
|
|
|
- getPrompt: () => Effect.succeed(undefined),
|
|
|
- readResource: () => Effect.succeed(undefined),
|
|
|
- startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
|
|
|
- authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
|
|
|
- finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
|
|
|
- removeAuth: () => Effect.void,
|
|
|
- supportsOAuth: () => Effect.succeed(false),
|
|
|
- hasStoredTokens: () => Effect.succeed(false),
|
|
|
- getAuthStatus: () => Effect.succeed("not_authenticated" as const),
|
|
|
- }),
|
|
|
-)
|
|
|
+function makeMcp(instructions: MCP.ServerInstructions[] = []) {
|
|
|
+ return Layer.succeed(
|
|
|
+ MCP.Service,
|
|
|
+ MCP.Service.of({
|
|
|
+ status: () => Effect.succeed({}),
|
|
|
+ clients: () => Effect.succeed({}),
|
|
|
+ instructions: () => Effect.succeed(instructions),
|
|
|
+ tools: () => Effect.succeed({}),
|
|
|
+ prompts: () => Effect.succeed({}),
|
|
|
+ resources: () => Effect.succeed({}),
|
|
|
+ resourceTemplates: () => Effect.succeed({}),
|
|
|
+ add: () => Effect.succeed({ status: { status: "disabled" as const } }),
|
|
|
+ connect: () => Effect.void,
|
|
|
+ disconnect: () => Effect.void,
|
|
|
+ getPrompt: () => Effect.succeed(undefined),
|
|
|
+ readResource: () => Effect.succeed(undefined),
|
|
|
+ startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
|
|
|
+ authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
|
|
|
+ finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
|
|
|
+ removeAuth: () => Effect.void,
|
|
|
+ supportsOAuth: () => Effect.succeed(false),
|
|
|
+ hasStoredTokens: () => Effect.succeed(false),
|
|
|
+ getAuthStatus: () => Effect.succeed("not_authenticated" as const),
|
|
|
+ }),
|
|
|
+ )
|
|
|
+}
|
|
|
|
|
|
const lsp = Layer.succeed(
|
|
|
LSP.Service,
|
|
|
@@ -164,7 +168,10 @@ const blockingProcessor = Layer.succeed(
|
|
|
}),
|
|
|
)
|
|
|
|
|
|
-function makePrompt(input?: { processor?: "blocking" }) {
|
|
|
+function makePrompt(input?: {
|
|
|
+ mcpInstructions?: MCP.ServerInstructions[]
|
|
|
+ processor?: "blocking"
|
|
|
+}) {
|
|
|
const deps = Layer.mergeAll(
|
|
|
Session.defaultLayer,
|
|
|
Snapshot.defaultLayer,
|
|
|
@@ -177,7 +184,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
|
|
|
Config.defaultLayer,
|
|
|
ProviderSvc.defaultLayer,
|
|
|
lsp,
|
|
|
- mcp,
|
|
|
+ makeMcp(input?.mcpInstructions),
|
|
|
FSUtil.defaultLayer,
|
|
|
BackgroundJob.defaultLayer,
|
|
|
status,
|
|
|
@@ -223,24 +230,47 @@ function makePrompt(input?: { processor?: "blocking" }) {
|
|
|
Layer.provideMerge(registry),
|
|
|
Layer.provideMerge(trunc),
|
|
|
Layer.provide(Instruction.defaultLayer),
|
|
|
- Layer.provide(SystemPrompt.defaultLayer),
|
|
|
+ Layer.provide(
|
|
|
+ SystemPrompt.layer.pipe(
|
|
|
+ Layer.provide(Skill.defaultLayer),
|
|
|
+ Layer.provide(LocationServiceMap.layer),
|
|
|
+ Layer.provide(deps),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
|
|
|
Layer.provideMerge(deps),
|
|
|
Layer.provide(summary),
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function makeHttp(input?: { processor?: "blocking" }) {
|
|
|
+function makeHttp(input?: {
|
|
|
+ mcpInstructions?: MCP.ServerInstructions[]
|
|
|
+ processor?: "blocking"
|
|
|
+}) {
|
|
|
return Layer.mergeAll(TestLLMServer.layer, makePrompt(input))
|
|
|
}
|
|
|
|
|
|
-function makeHttpNoLLMServer(input?: { processor?: "blocking" }) {
|
|
|
+function makeHttpNoLLMServer(input?: {
|
|
|
+ mcpInstructions?: MCP.ServerInstructions[]
|
|
|
+ processor?: "blocking"
|
|
|
+}) {
|
|
|
return makePrompt(input)
|
|
|
}
|
|
|
|
|
|
const it = testEffect(makeHttp())
|
|
|
const noLLMServer = testEffect(makeHttpNoLLMServer())
|
|
|
const raceNoLLMServer = testEffect(makeHttpNoLLMServer({ processor: "blocking" }))
|
|
|
+const withMcpInstructions = testEffect(
|
|
|
+ makeHttp({
|
|
|
+ mcpInstructions: [
|
|
|
+ {
|
|
|
+ name: "guide-server",
|
|
|
+ instructions: "Use lookup before mutate.",
|
|
|
+ tools: ["guide-server_lookup"],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ }),
|
|
|
+)
|
|
|
const unix = process.platform !== "win32" ? it.instance : it.instance.skip
|
|
|
const unixNoLLMServer = process.platform !== "win32" ? noLLMServer.instance : noLLMServer.instance.skip
|
|
|
|
|
|
@@ -507,6 +537,30 @@ it.instance("loop calls LLM and returns assistant message", () =>
|
|
|
}),
|
|
|
)
|
|
|
|
|
|
+withMcpInstructions.instance("loop includes MCP instructions in model system context", () =>
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const { llm } = yield* useServerConfig(providerCfg)
|
|
|
+ const prompt = yield* SessionPrompt.Service
|
|
|
+ const sessions = yield* Session.Service
|
|
|
+ const chat = yield* sessions.create({
|
|
|
+ title: "Pinned",
|
|
|
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
|
|
+ })
|
|
|
+ yield* llm.hang
|
|
|
+ yield* user(chat.id, "hello")
|
|
|
+
|
|
|
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
|
|
|
+ yield* awaitWithTimeout(llm.wait(1), "timed out waiting for MCP instruction request", "10 seconds")
|
|
|
+
|
|
|
+ const hits = yield* llm.hits
|
|
|
+ const body = JSON.stringify(hits[0]?.body)
|
|
|
+ expect(body).toContain('<server name=\\"guide-server\\">')
|
|
|
+ expect(body).toContain("Use lookup before mutate.")
|
|
|
+ yield* Fiber.interrupt(fiber)
|
|
|
+ }),
|
|
|
+ 15_000,
|
|
|
+)
|
|
|
+
|
|
|
it.instance("loop surfaces content-filter finishes as session errors", () =>
|
|
|
Effect.gen(function* () {
|
|
|
const { llm } = yield* useServerConfig(providerCfg)
|