feat: 品牌替换 + 启动优化 + AGENTS.md 模板定制
- 品牌替换:OpenCode/opencode → AirCoding/aircoding(16+ 文件) - Logo ASCII art:修复 left/right 行数不匹配导致的启动崩溃 - 启动诊断:添加 OPENCODE_PRINT_TIMING 计时探针 - dev 模式默认 --pure 跳过外部插件加载 - AGENTS.md 模板:追加 AirCoding 多 Agent 专项段落 - architect prompt + plugin:强化 AGENTS.md 产出验证
This commit is contained in:
186
packages/llm/src/llm.ts
Normal file
186
packages/llm/src/llm.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Effect, JsonSchema, Schema } from "effect"
|
||||
import { LLMClient } from "./route/client"
|
||||
import {
|
||||
GenerationOptions,
|
||||
HttpOptions,
|
||||
InvalidProviderOutputReason,
|
||||
LLMError,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Message,
|
||||
type ModelInput as SchemaModelInput,
|
||||
SystemPart,
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
type ContentPart,
|
||||
ToolResultPart,
|
||||
} from "./schema"
|
||||
import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
|
||||
|
||||
export type ModelInput = SchemaModelInput
|
||||
|
||||
export type MessageInput = Message.Input
|
||||
|
||||
export type ToolChoiceInput = ToolChoice.Input
|
||||
export type ToolChoiceMode = ToolChoice.Mode
|
||||
|
||||
export type ToolResultInput = Parameters<typeof ToolResultPart.make>[0]
|
||||
|
||||
/** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
|
||||
export type RequestInput = Omit<
|
||||
ConstructorParameters<typeof LLMRequest>[0],
|
||||
"system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
|
||||
> & {
|
||||
readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
|
||||
readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
|
||||
readonly messages?: ReadonlyArray<Message | MessageInput>
|
||||
readonly tools?: ReadonlyArray<ToolDefinition.Input>
|
||||
readonly toolChoice?: ToolChoiceInput
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: ConstructorParameters<typeof LLMRequest>[0]["providerOptions"]
|
||||
readonly http?: HttpOptions.Input
|
||||
}
|
||||
|
||||
export const generate = LLMClient.generate
|
||||
|
||||
export const stream = LLMClient.stream
|
||||
|
||||
export const requestInput = (input: LLMRequest): RequestInput => ({
|
||||
...LLMRequest.input(input),
|
||||
})
|
||||
|
||||
export const request = (input: RequestInput) => {
|
||||
const {
|
||||
system: requestSystem,
|
||||
prompt,
|
||||
messages,
|
||||
tools,
|
||||
toolChoice: requestToolChoice,
|
||||
generation: requestGeneration,
|
||||
providerOptions: requestProviderOptions,
|
||||
http: requestHttp,
|
||||
...rest
|
||||
} = input
|
||||
return new LLMRequest({
|
||||
...rest,
|
||||
system: SystemPart.content(requestSystem),
|
||||
messages: [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])],
|
||||
tools: tools?.map(ToolDefinition.make) ?? [],
|
||||
toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined,
|
||||
generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration),
|
||||
providerOptions: requestProviderOptions,
|
||||
http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp),
|
||||
})
|
||||
}
|
||||
|
||||
export const updateRequest = (input: LLMRequest, patch: Partial<RequestInput>) =>
|
||||
request({ ...requestInput(input), ...patch })
|
||||
|
||||
const GENERATE_OBJECT_TOOL_NAME = "generate_object"
|
||||
|
||||
const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
|
||||
|
||||
type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice" | "responseFormat">
|
||||
|
||||
export class GenerateObjectResponse<T> {
|
||||
constructor(
|
||||
readonly object: T,
|
||||
readonly response: LLMResponse,
|
||||
) {}
|
||||
|
||||
get events() {
|
||||
return this.response.events
|
||||
}
|
||||
|
||||
get usage() {
|
||||
return this.response.usage
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenerateObjectOptions<S extends ToolSchema<any>> extends GenerateObjectBase {
|
||||
readonly schema: S
|
||||
}
|
||||
|
||||
export interface GenerateObjectDynamicOptions extends GenerateObjectBase {
|
||||
/** Raw JSON Schema object describing the expected output shape. */
|
||||
readonly jsonSchema: JsonSchema.JsonSchema
|
||||
}
|
||||
|
||||
const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
||||
options: GenerateObjectBase,
|
||||
tool: ReturnType<typeof makeTool>,
|
||||
) {
|
||||
const baseRequest = request(options)
|
||||
const generateRequest = LLMRequest.update(baseRequest, {
|
||||
tools: toDefinitions({ [GENERATE_OBJECT_TOOL_NAME]: tool }),
|
||||
toolChoice: ToolChoice.named(GENERATE_OBJECT_TOOL_NAME),
|
||||
})
|
||||
const response = yield* LLMClient.generate(generateRequest)
|
||||
const call = response.toolCalls.find(
|
||||
(event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME,
|
||||
)
|
||||
if (!call || !LLMEvent.is.toolCall(call))
|
||||
return yield* new LLMError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`,
|
||||
}),
|
||||
})
|
||||
const object = yield* tool._decode(call.input).pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new LLMError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
message: `generateObject: tool input failed schema decode: ${error.message}`,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
return new GenerateObjectResponse(object, response)
|
||||
})
|
||||
|
||||
/**
|
||||
* Run a model and decode its output against `schema`. Works on every protocol
|
||||
* because it forces a synthetic tool call internally — provider-native JSON
|
||||
* modes are intentionally avoided so behaviour is uniform.
|
||||
*
|
||||
* Two input modes:
|
||||
*
|
||||
* 1. `schema: EffectSchema<T>` — `.object` is decoded and typed as `T`.
|
||||
* Decode failures surface as `LLMError`.
|
||||
* 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when
|
||||
* the schema is only available at runtime (MCP, plugin manifests). Caller validates.
|
||||
*/
|
||||
export function generateObject<S extends ToolSchema<any>>(
|
||||
options: GenerateObjectOptions<S>,
|
||||
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
|
||||
export function generateObject(
|
||||
options: GenerateObjectDynamicOptions,
|
||||
): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
|
||||
export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
|
||||
if ("schema" in options) {
|
||||
const { schema, ...rest } = options
|
||||
return runGenerateObject(
|
||||
rest,
|
||||
makeTool({
|
||||
description: GENERATE_OBJECT_TOOL_DESCRIPTION,
|
||||
parameters: schema,
|
||||
success: Schema.Unknown as ToolSchema<unknown>,
|
||||
execute: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const { jsonSchema, ...rest } = options
|
||||
return runGenerateObject(
|
||||
rest,
|
||||
makeTool({
|
||||
description: GENERATE_OBJECT_TOOL_DESCRIPTION,
|
||||
jsonSchema,
|
||||
execute: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user