Skip to content
Closed
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
75 changes: 20 additions & 55 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai"
import type { ModelMessage } from "ai"
import { Instance } from "../project/instance"
import { Truncate } from "../tool/truncate"
import { Auth } from "../auth"
import { ProviderTransform } from "../provider/transform"

import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
Expand All @@ -17,11 +16,11 @@ import { Permission } from "@/permission"
import { mergeDeep, pipe, sortBy, values } from "remeda"
import { Global } from "@/global"
import path from "path"
import { Plugin } from "@/plugin"
import { Skill } from "../skill"
import { Effect, ServiceMap, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { LLM } from "@/session/llm"

export namespace Agent {
export const Info = z
Expand Down Expand Up @@ -73,7 +72,6 @@ export namespace Agent {
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const auth = yield* Auth.Service
const skill = yield* Skill.Service
const provider = yield* Provider.Service

Expand Down Expand Up @@ -330,64 +328,31 @@ export namespace Agent {
description: string
model?: { providerID: ProviderID; modelID: ModelID }
}) {
const cfg = yield* config.get()
const model = input.model ?? (yield* provider.defaultModel())
const resolved = yield* provider.getModel(model.providerID, model.modelID)
const language = yield* provider.getLanguage(resolved)

const system = [PROMPT_GENERATE]
yield* Effect.promise(() =>
Plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system }),
)
const existing = yield* InstanceState.useEffect(state, (s) => s.list())

const params = {
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
metadata: {
userId: cfg.username ?? "unknown",
},
},
temperature: 0.3,
messages: [
...system.map(
(item): ModelMessage => ({
role: "system",
content: item,
}),
),
{
role: "user",
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
},
],
model: language,
schema: z.object({
identifier: z.string(),
whenToUse: z.string(),
systemPrompt: z.string(),
}),
} satisfies Parameters<typeof generateObject>[0]
const USER_MESSAGE_CONTENT = {
role: "user",
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
} as ModelMessage

// TODO: clean this up so provider specific logic doesnt bleed over
const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
if (model.providerID === "openai" && authInfo?.type === "oauth") {
return yield* Effect.promise(async () => {
const result = streamObject({
...params,
providerOptions: ProviderTransform.providerOptions(resolved, {
store: false,
}),
onError: () => {},
})
for await (const part of result.fullStream) {
if (part.type === "error") throw part.error
}
return result.object
})
}

return yield* Effect.promise(() => generateObject(params).then((r) => r.object))
return yield* Effect.promise((abort) =>
LLM.generateObject({
abort,
temperature: 0.3,
messages: [USER_MESSAGE_CONTENT],
model: resolved,
system,
schema: z.object({
identifier: z.string(),
whenToUse: z.string(),
systemPrompt: z.string(),
}),
}),
)
}),
})
}),
Expand Down
76 changes: 75 additions & 1 deletion packages/opencode/src/session/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import { Log } from "@/util/log"
import { Cause, Effect, Layer, Record, ServiceMap } from "effect"
import * as Queue from "effect/Queue"
import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
import {
generateObject as generateObjectAI,
streamText,
streamObject,
wrapLanguageModel,
type ModelMessage,
type Tool,
tool,
jsonSchema,
} from "ai"
import { mergeDeep, pipe } from "remeda"
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
import { ProviderTransform } from "@/provider/transform"
Expand All @@ -21,6 +30,7 @@ import { Wildcard } from "@/util/wildcard"
import { SessionID } from "@/session/schema"
import { Auth } from "@/auth"
import { Installation } from "@/installation"
import z from "zod"

export namespace LLM {
const log = Log.create({ service: "llm" })
Expand All @@ -45,6 +55,18 @@ export namespace LLM {
abort: AbortSignal
}

export type ObjectInput<Schema extends z.ZodType> = {
model: Provider.Model
system: string[]
messages: ModelMessage[]
schema: Schema
temperature?: number
}

export type ObjectRequest<Schema extends z.ZodType> = ObjectInput<Schema> & {
abort: AbortSignal
}

export type Event = Awaited<ReturnType<typeof stream>>["fullStream"] extends AsyncIterable<infer T> ? T : never

export interface Interface {
Expand Down Expand Up @@ -390,6 +412,58 @@ export namespace LLM {
})
}

export async function generateObject<Schema extends z.ZodType>(input: ObjectRequest<Schema>) {
const [language, cfg, provider, auth] = await Promise.all([
Provider.getLanguage(input.model),
Config.get(),
Provider.getProvider(input.model.providerID),
Auth.get(input.model.providerID),
])
const system = [...input.system]
await Plugin.trigger("experimental.chat.system.transform", { model: input.model }, { system })
const params = {
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
metadata: {
userId: cfg.username ?? "unknown",
},
},
temperature: input.temperature,
messages:
provider.id === "openai" && auth?.type === "oauth"
? input.messages
: [
...system.map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
...input.messages,
],
model: language,
schema: input.schema,
abortSignal: input.abort,
} satisfies Parameters<typeof generateObjectAI<Schema>>[0]

if (provider.id === "openai" && auth?.type === "oauth") {
const result = streamObject({
...params,
providerOptions: ProviderTransform.providerOptions(input.model, {
instructions: system.join("\n"),
store: false,
}),
onError: () => {},
})
for await (const part of result.fullStream) {
if (part.type === "error") throw part.error
}
return result.object
}

return generateObjectAI(params).then((x) => x.object)
}

function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
const disabled = Permission.disabled(
Object.keys(input.tools),
Expand Down
Loading