chore: generate

This commit is contained in:
opencode-agent[bot] 2026-06-04 03:03:39 +00:00
commit b0a929440b
87 changed files with 2301 additions and 1599 deletions

View file

@ -11,7 +11,9 @@ import { ToolRegistry } from "../tool-registry"
export const name = "apply_patch"
export const Parameters = Schema.Struct({
patchText: Schema.String.annotate({ description: "The full patch text describing add, update, and delete operations" }),
patchText: Schema.String.annotate({
description: "The full patch text describing add, update, and delete operations",
}),
})
export const Applied = Schema.Struct({
@ -24,7 +26,12 @@ export const Success = Schema.Struct({ applied: Schema.Array(Applied) })
export type Success = typeof Success.Type
export const toModelOutput = (output: Success) =>
["Applied patch sequentially:", ...output.applied.map((item) => `${item.type === "add" ? "A" : item.type === "delete" ? "D" : "M"} ${item.resource}`)].join("\n")
[
"Applied patch sequentially:",
...output.applied.map(
(item) => `${item.type === "add" ? "A" : item.type === "delete" ? "D" : "M"} ${item.resource}`,
),
].join("\n")
const definition = Tool.make({
description:
@ -36,9 +43,23 @@ const definition = Tool.make({
type Planned = { readonly hunk: Patch.Hunk; readonly plan: LocationMutation.Plan }
type Prepared =
| { readonly type: "add"; readonly hunk: Extract<Patch.Hunk, { readonly type: "add" }>; readonly plan: LocationMutation.Plan }
| { readonly type: "delete"; readonly hunk: Extract<Patch.Hunk, { readonly type: "delete" }>; readonly plan: LocationMutation.Plan }
| { readonly type: "update"; readonly hunk: Extract<Patch.Hunk, { readonly type: "update" }>; readonly plan: LocationMutation.Plan; readonly source: Uint8Array; readonly content: string }
| {
readonly type: "add"
readonly hunk: Extract<Patch.Hunk, { readonly type: "add" }>
readonly plan: LocationMutation.Plan
}
| {
readonly type: "delete"
readonly hunk: Extract<Patch.Hunk, { readonly type: "delete" }>
readonly plan: LocationMutation.Plan
}
| {
readonly type: "update"
readonly hunk: Extract<Patch.Hunk, { readonly type: "update" }>
readonly plan: LocationMutation.Plan
readonly source: Uint8Array
readonly content: string
}
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
@ -53,9 +74,10 @@ export const layer = Layer.effectDiscard(
execute: ({ parameters, assertPermission }) => {
const applied: Array<typeof Applied.Type> = []
const fail = (path: string, cause: unknown) => {
const prefix = applied.length === 0
? `Unable to apply patch at ${path}`
: `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}`
const prefix =
applied.length === 0
? `Unable to apply patch at ${path}`
: `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}`
return new ToolFailure({ message: prefix, error: cause })
}
return Effect.gen(function* () {
@ -69,7 +91,8 @@ export const layer = Layer.effectDiscard(
if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" })
const planned: Planned[] = []
for (const hunk of hunks) planned.push({ hunk, plan: yield* mutation.resolve({ path: hunk.path, kind: "file" }) })
for (const hunk of hunks)
planned.push({ hunk, plan: yield* mutation.resolve({ path: hunk.path, kind: "file" }) })
const externalDirectories = new Map<string, LocationMutation.ExternalDirectoryAuthorization>()
for (const { plan } of planned) {
const external = plan.target.externalDirectory
@ -78,7 +101,11 @@ export const layer = Layer.effectDiscard(
for (const external of externalDirectories.values()) {
yield* assertPermission(LocationMutation.externalDirectoryPermission(external))
}
yield* assertPermission({ action: "edit", resources: [...new Set(planned.map(({ plan }) => plan.target.resource))], save: ["*"] })
yield* assertPermission({
action: "edit",
resources: [...new Set(planned.map(({ plan }) => plan.target.resource))],
save: ["*"],
})
const prepared: Prepared[] = []
for (const { hunk, plan } of planned) {
@ -89,33 +116,51 @@ export const layer = Layer.effectDiscard(
continue
}
const target = yield* mutation.revalidate(plan)
if (!target.exists || target.type !== "File") return yield* fail(hunk.path, new Error("Target file does not exist"))
if (!target.exists || target.type !== "File")
return yield* fail(hunk.path, new Error("Target file does not exist"))
if (hunk.type === "delete") {
prepared.push({ type: hunk.type, hunk, plan })
continue
}
const source = yield* fs.readFile(target.canonical)
const update = Patch.derive(hunk.path, hunk.chunks, new TextDecoder("utf-8", { ignoreBOM: true }).decode(source))
const update = Patch.derive(
hunk.path,
hunk.chunks,
new TextDecoder("utf-8", { ignoreBOM: true }).decode(source),
)
prepared.push({ type: hunk.type, hunk, plan, source, content: Patch.joinBom(update.content, update.bom) })
}
yield* Effect.uninterruptible(
Effect.forEach(prepared, (change) =>
Effect.gen(function* () {
if (change.type === "add") {
const result = yield* files.create({ plan: change.plan, content: change.hunk.contents.endsWith("\n") || change.hunk.contents === "" ? change.hunk.contents : `${change.hunk.contents}\n` })
Effect.forEach(
prepared,
(change) =>
Effect.gen(function* () {
if (change.type === "add") {
const result = yield* files.create({
plan: change.plan,
content:
change.hunk.contents.endsWith("\n") || change.hunk.contents === ""
? change.hunk.contents
: `${change.hunk.contents}\n`,
})
applied.push({ type: change.type, resource: result.resource, target: result.target })
return
}
if (change.type === "delete") {
const result = yield* files.remove({ plan: change.plan })
applied.push({ type: change.type, resource: result.resource, target: result.target })
return
}
const result = yield* files.writeIfUnchanged({
plan: change.plan,
expected: change.source,
content: change.content,
})
applied.push({ type: change.type, resource: result.resource, target: result.target })
return
}
if (change.type === "delete") {
const result = yield* files.remove({ plan: change.plan })
applied.push({ type: change.type, resource: result.resource, target: result.target })
return
}
const result = yield* files.writeIfUnchanged({ plan: change.plan, expected: change.source, content: change.content })
applied.push({ type: change.type, resource: result.resource, target: result.target })
}).pipe(Effect.catchCause((cause) => Effect.fail(fail(change.hunk.path, Cause.squash(cause))))),
{ discard: true }),
}).pipe(Effect.catchCause((cause) => Effect.fail(fail(change.hunk.path, Cause.squash(cause))))),
{ discard: true },
),
)
return { applied }
}).pipe(

View file

@ -112,7 +112,9 @@ export const layer = Layer.effectDiscard(
const error = Cause.squash(cause)
return Effect.fail(
error instanceof FileMutation.StaleContentError
? new ToolFailure({ message: "File changed after permission approval. Read it again before editing." })
? new ToolFailure({
message: "File changed after permission approval. Read it again before editing.",
})
: new ToolFailure({ message: `Unable to edit ${parameters.path}`, error }),
)
}),
@ -123,7 +125,9 @@ export const layer = Layer.effectDiscard(
return yield* new ToolFailure({ message: "No changes to apply: oldString and newString are identical." })
}
if (parameters.oldString === "") {
return yield* new ToolFailure({ message: "oldString must not be empty. Use write to create or overwrite a file." })
return yield* new ToolFailure({
message: "oldString must not be empty. Use write to create or overwrite a file.",
})
}
const plan = yield* unableToEdit(mutation.resolve({ path: parameters.path, kind: "file" }))
@ -158,7 +162,11 @@ export const layer = Layer.effectDiscard(
: source.text.replace(oldString, newString)
const next = splitBom(replaced)
const result = yield* unableToEdit(
files.writeIfUnchanged({ plan, expected: source.content, content: joinBom(next.text, source.bom || next.bom) }),
files.writeIfUnchanged({
plan,
expected: source.content,
content: joinBom(next.text, source.bom || next.bom),
}),
)
return { ...result, replacements } satisfies Success
})

View file

@ -10,9 +10,15 @@ export const name = "glob"
export const Parameters = Schema.Struct({
pattern: LocationSearch.FilesInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }),
path: LocationSearch.FilesInput.fields.path.annotate({ description: "Relative directory to search. Defaults to the active Location." }),
reference: LocationSearch.FilesInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }),
limit: LocationSearch.FilesInput.fields.limit.annotate({ description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }),
path: LocationSearch.FilesInput.fields.path.annotate({
description: "Relative directory to search. Defaults to the active Location.",
}),
reference: LocationSearch.FilesInput.fields.reference.annotate({
description: "Named project reference to search instead of the active Location",
}),
limit: LocationSearch.FilesInput.fields.limit.annotate({
description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
}),
})
type ModelOutput = typeof LocationSearch.FilesResult.Encoded
@ -21,14 +27,18 @@ type ModelOutput = typeof LocationSearch.FilesResult.Encoded
export const toModelOutput = (output: ModelOutput) => {
const lines = output.items.length === 0 ? ["No files found"] : output.items.map((item) => item.resource)
if (output.truncated) {
lines.push("", `(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`)
lines.push(
"",
`(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`,
)
}
if (output.partial) lines.push("", "(Results may be incomplete because some discovered files could not be read.)")
return lines.join("\n")
}
const definition = Tool.make({
description: "Find files by glob pattern within the active Location or a named project reference. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
description:
"Find files by glob pattern within the active Location or a named project reference. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
parameters: Parameters,
success: LocationSearch.FilesResult,
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
@ -66,7 +76,12 @@ export const layer = Layer.effectDiscard(
return yield* search.files(parameters, root)
}).pipe(
Effect.catchCause((cause) =>
Effect.fail(new ToolFailure({ message: `Unable to find files matching ${parameters.pattern}`, error: Cause.squash(cause) })),
Effect.fail(
new ToolFailure({
message: `Unable to find files matching ${parameters.pattern}`,
error: Cause.squash(cause),
}),
),
),
),
}),

View file

@ -10,11 +10,21 @@ import { ToolRegistry } from "../tool-registry"
export const name = "grep"
export const Parameters = Schema.Struct({
pattern: LocationSearch.GrepInput.fields.pattern.annotate({ description: "Regex pattern to search for in file contents" }),
path: LocationSearch.GrepInput.fields.path.annotate({ description: "Relative file or directory to search. Defaults to the active Location." }),
reference: LocationSearch.GrepInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }),
include: LocationSearch.GrepInput.fields.include.annotate({ description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")' }),
limit: LocationSearch.GrepInput.fields.limit.annotate({ description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }),
pattern: LocationSearch.GrepInput.fields.pattern.annotate({
description: "Regex pattern to search for in file contents",
}),
path: LocationSearch.GrepInput.fields.path.annotate({
description: "Relative file or directory to search. Defaults to the active Location.",
}),
reference: LocationSearch.GrepInput.fields.reference.annotate({
description: "Named project reference to search instead of the active Location",
}),
include: LocationSearch.GrepInput.fields.include.annotate({
description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")',
}),
limit: LocationSearch.GrepInput.fields.limit.annotate({
description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
}),
})
type Success = typeof LocationSearch.GrepResult.Encoded
@ -32,14 +42,18 @@ export const toModelOutput = (output: Success) => {
lines.push(` Line ${match.line}: ${match.lines}${match.linePreviewTruncated ? "..." : ""}`)
}
if (output.truncated) {
lines.push("", `(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`)
lines.push(
"",
`(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`,
)
}
if (output.partial) lines.push("", "(Some paths were inaccessible and skipped)")
return lines.join("\n")
}
const definition = Tool.make({
description: "Search file contents by regular expression within the active Location or a named project reference. Use a relative path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise relative file resources, line numbers, and bounded line previews.",
description:
"Search file contents by regular expression within the active Location or a named project reference. Use a relative path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise relative file resources, line numbers, and bounded line previews.",
parameters: Parameters,
success: LocationSearch.GrepResult,
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
@ -79,9 +93,10 @@ export const layer = Layer.effectDiscard(
}).pipe(
Effect.catchCause((cause) => {
const error = Cause.squash(cause)
const message = error instanceof Ripgrep.InvalidPatternError
? `Invalid grep pattern ${JSON.stringify(parameters.pattern)}: ${error.message}`
: `Unable to grep for ${parameters.pattern}`
const message =
error instanceof Ripgrep.InvalidPatternError
? `Invalid grep pattern ${JSON.stringify(parameters.pattern)}: ${error.message}`
: `Unable to grep for ${parameters.pattern}`
return Effect.fail(new ToolFailure({ message, error }))
}),
),

View file

@ -70,7 +70,11 @@ export const layer = Layer.effectDiscard(
const final = yield* filesystem.resolveReadPath(input)
if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real)
return yield* Effect.die(new Error("File changed after permission approval"))
if (final.target.size > FileSystem.MAX_READ_BYTES || input.offset !== undefined || input.limit !== undefined)
if (
final.target.size > FileSystem.MAX_READ_BYTES ||
input.offset !== undefined ||
input.limit !== undefined
)
return yield* filesystem.readTextPageResolved(final.target, { offset: input.offset, limit: input.limit })
return yield* filesystem.readResolved(final.target, FileSystem.MAX_READ_BYTES)
}).pipe(

View file

@ -150,7 +150,9 @@ export const layer = Layer.effectDiscard(
const { body, contentType } = yield* Effect.gen(function* () {
const response = yield* execute(http, parameters.url, parameters.format).pipe(
Effect.catchIf(isCloudflareChallenge, () => execute(http, parameters.url, parameters.format, "opencode")),
Effect.catchIf(isCloudflareChallenge, () =>
execute(http, parameters.url, parameters.format, "opencode"),
),
)
const contentType = response.headers["content-type"] || ""
const mime = mimeFrom(contentType)

View file

@ -34,7 +34,9 @@ The current year is ${new Date().getFullYear()}. Use this year when searching fo
export const Parameters = Schema.Struct({
query: Schema.String.annotate({ description: "Websearch query" }),
numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({ description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})` }),
numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({
description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})`,
}),
livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
description:
"Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
@ -42,9 +44,11 @@ export const Parameters = Schema.Struct({
type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
}),
contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate({
description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`,
}),
contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate(
{
description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`,
},
),
})
export const Provider = Schema.Literals(["exa", "parallel"])
@ -63,13 +67,11 @@ export class ConfigService extends Context.Service<ConfigService, Config>()("@op
/** Isolates the retained product environment contract from the generic tool implementation. */
export const defaultConfigLayer = Layer.sync(ConfigService, () =>
ConfigService.of({
provider: process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
? process.env.OPENCODE_WEBSEARCH_PROVIDER
: undefined,
enableExa:
truthy("OPENCODE_EXPERIMENTAL") ||
truthy("OPENCODE_ENABLE_EXA") ||
truthy("OPENCODE_EXPERIMENTAL_EXA"),
provider:
process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
? process.env.OPENCODE_WEBSEARCH_PROVIDER
: undefined,
enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"),
enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
exaApiKey: process.env.EXA_API_KEY,
parallelApiKey: process.env.PARALLEL_API_KEY,
@ -162,9 +164,15 @@ const callMcp = <F extends Schema.Struct.Fields>(
return yield* Effect.gen(function* () {
const response = yield* HttpClient.filterStatusOk(http).execute(request)
const body = yield* response.text
if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES) return yield* Effect.die(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`))
if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES)
return yield* Effect.die(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`))
return yield* parseResponse(body)
}).pipe(Effect.timeoutOrElse({ duration: Duration.seconds(25), orElse: () => Effect.die(new Error(`${tool} request timed out`)) }))
}).pipe(
Effect.timeoutOrElse({
duration: Duration.seconds(25),
orElse: () => Effect.die(new Error(`${tool} request timed out`)),
}),
)
})
const Success = Schema.Struct({
@ -201,30 +209,31 @@ export const layer = Layer.effectDiscard(
metadata: { ...parameters, provider },
})
const text = provider === "exa"
? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, {
query: parameters.query,
type: parameters.type || "auto",
numResults: parameters.numResults || 8,
livecrawl: parameters.livecrawl || "fallback",
contextMaxCharacters: parameters.contextMaxCharacters,
})
: yield* callMcp(
http,
PARALLEL_URL,
"web_search",
ParallelArgs,
{
objective: parameters.query,
search_queries: [parameters.query],
session_id: sessionID,
// V2 invocation context does not safely expose the model yet.
},
{
"User-Agent": `opencode/${InstallationVersion}`,
...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}),
},
)
const text =
provider === "exa"
? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, {
query: parameters.query,
type: parameters.type || "auto",
numResults: parameters.numResults || 8,
livecrawl: parameters.livecrawl || "fallback",
contextMaxCharacters: parameters.contextMaxCharacters,
})
: yield* callMcp(
http,
PARALLEL_URL,
"web_search",
ParallelArgs,
{
objective: parameters.query,
search_queries: [parameters.query],
session_id: sessionID,
// V2 invocation context does not safely expose the model yet.
},
{
"User-Agent": `opencode/${InstallationVersion}`,
...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}),
},
)
const truncated = yield* resources.truncate({ sessionID, toolCallID: call.id, content: text ?? NO_RESULTS })
return {
provider,
@ -234,7 +243,12 @@ export const layer = Layer.effectDiscard(
}
}).pipe(
Effect.catchCause((cause) =>
Effect.fail(new ToolFailure({ message: `Unable to search the web for ${parameters.query}`, error: Cause.squash(cause) })),
Effect.fail(
new ToolFailure({
message: `Unable to search the web for ${parameters.query}`,
error: Cause.squash(cause),
}),
),
),
)
},