refactor(core): simplify tool infrastructure (#43967)
This commit is contained in:
@@ -90,31 +90,20 @@ export function applyOnly(db: Database, input: Migration[]) {
|
||||
)
|
||||
}),
|
||||
)
|
||||
if (migration.foreignKeys !== false) {
|
||||
yield* apply.pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logError("database migration failed", {
|
||||
migration: migration.id,
|
||||
durationMs: Date.now() - started,
|
||||
error,
|
||||
}),
|
||||
),
|
||||
)
|
||||
yield* Effect.logInfo("database migration completed", {
|
||||
migration: migration.id,
|
||||
durationMs: Date.now() - started,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// Durable Object SQLite rejects the foreign_keys toggle; the closest
|
||||
// allowlisted relaxation is deferring enforcement to transaction commit.
|
||||
const relaxForeignKeys = supportsForeignKeyToggle
|
||||
? db.run(sql`PRAGMA foreign_keys = OFF`)
|
||||
: db.run(sql`PRAGMA defer_foreign_keys = ON`)
|
||||
const restoreForeignKeys = supportsForeignKeyToggle ? db.run(sql`PRAGMA foreign_keys = ON`) : Effect.void
|
||||
yield* relaxForeignKeys
|
||||
yield* apply.pipe(
|
||||
Effect.ensuring(restoreForeignKeys.pipe(Effect.orDie)),
|
||||
const run =
|
||||
migration.foreignKeys !== false
|
||||
? apply
|
||||
: Effect.gen(function* () {
|
||||
// Durable Object SQLite rejects the foreign_keys toggle; the closest
|
||||
// allowlisted relaxation is deferring enforcement to transaction commit.
|
||||
const relaxForeignKeys = supportsForeignKeyToggle
|
||||
? db.run(sql`PRAGMA foreign_keys = OFF`)
|
||||
: db.run(sql`PRAGMA defer_foreign_keys = ON`)
|
||||
const restoreForeignKeys = supportsForeignKeyToggle ? db.run(sql`PRAGMA foreign_keys = ON`) : Effect.void
|
||||
yield* relaxForeignKeys
|
||||
yield* apply.pipe(Effect.ensuring(restoreForeignKeys.pipe(Effect.orDie)))
|
||||
})
|
||||
yield* run.pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logError("database migration failed", {
|
||||
migration: migration.id,
|
||||
|
||||
@@ -4,16 +4,13 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
|
||||
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements: LayerNode.Replacements = []) {
|
||||
let allReplacements = replacements
|
||||
|
||||
// Only build the location service map if it's actually needed
|
||||
if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) {
|
||||
const locationMap = buildLocationServiceMap(replacements)
|
||||
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
||||
allReplacements = replacements.concat([[LocationServiceMap.node, locationMapNode]])
|
||||
}
|
||||
if (!LayerNode.hasUnbound(root, LocationServiceMap.node) || hasReplacement(replacements, LocationServiceMap.node))
|
||||
return LayerNode.compile(root, replacements)
|
||||
|
||||
return LayerNode.compile(root, allReplacements)
|
||||
const locationMap = buildLocationServiceMap(replacements)
|
||||
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
||||
return LayerNode.compile(root, replacements.concat([[LocationServiceMap.node, locationMapNode]]))
|
||||
}
|
||||
|
||||
function hasReplacement(replacements: LayerNode.Replacements, node: LayerNode.Node<unknown, unknown, any>) {
|
||||
|
||||
@@ -70,7 +70,7 @@ const baseLayer = Layer.effect(
|
||||
return yield* Effect.die(new Error("Path escapes the location"))
|
||||
const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
|
||||
if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the location"))
|
||||
return { absolute, real, directory: location.directory, root }
|
||||
return { absolute, real, directory: location.directory }
|
||||
})
|
||||
return Service.of({
|
||||
find: search.find,
|
||||
|
||||
@@ -286,10 +286,10 @@ export const Plugin = {
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
const files = yield* Effect.forEach(prepared, (change) => {
|
||||
if (change.type === "delete") return Effect.succeed(patchFile(change))
|
||||
const files = prepared.map((change) => {
|
||||
if (change.type === "delete") return patchFile(change)
|
||||
const target = change.type === "update" && change.moveTarget ? change.moveTarget : change.target
|
||||
return Effect.succeed(patchFile(change, formatted.get(target.absolute)))
|
||||
return patchFile(change, formatted.get(target.absolute))
|
||||
})
|
||||
return { applied, files }
|
||||
}).pipe(
|
||||
|
||||
@@ -94,6 +94,13 @@ const toolResult = (output: Output) => {
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundResult = (shellID: string) => ({
|
||||
output: BACKGROUND_STARTED,
|
||||
shellID,
|
||||
truncated: false,
|
||||
status: "running" as const,
|
||||
})
|
||||
|
||||
export const Plugin = {
|
||||
id: "opencode.tool.shell",
|
||||
effect: Effect.fn("ShellTool.Plugin")(function* (ctx: PluginContext) {
|
||||
@@ -303,12 +310,7 @@ export const Plugin = {
|
||||
if (input.background === true) {
|
||||
yield* runtime.job.background(job.id)
|
||||
yield* notifyWhenDone(context.sessionID, context.id, info.id, info.command, settled)
|
||||
return {
|
||||
output: BACKGROUND_STARTED,
|
||||
shellID: info.id,
|
||||
truncated: false,
|
||||
status: "running" as const,
|
||||
}
|
||||
return backgroundResult(info.id)
|
||||
}
|
||||
|
||||
const result = yield* runtime.job
|
||||
@@ -317,12 +319,7 @@ export const Plugin = {
|
||||
if (result?.type === "backgrounded") {
|
||||
yield* shell.timeout(info.id, 0)
|
||||
yield* notifyWhenDone(context.sessionID, context.id, info.id, info.command, settled)
|
||||
return {
|
||||
output: BACKGROUND_STARTED,
|
||||
shellID: info.id,
|
||||
truncated: false,
|
||||
status: "running" as const,
|
||||
}
|
||||
return backgroundResult(info.id)
|
||||
}
|
||||
if (result?.info.status === "error")
|
||||
return yield* Effect.fail(new Error(result.info.error ?? "Command failed"))
|
||||
|
||||
@@ -12,12 +12,15 @@ import { SessionSchema } from "../../session/schema.js"
|
||||
export const name = "subagent"
|
||||
|
||||
const NO_TEXT = "Subagent completed without a text response."
|
||||
const backgroundStarted = (sessionID: SessionSchema.ID) =>
|
||||
[
|
||||
const backgroundResult = (sessionID: SessionSchema.ID) => ({
|
||||
sessionID,
|
||||
status: "running" as const,
|
||||
output: [
|
||||
`The subagent is working in the background (sessionID: ${sessionID}). You will be notified automatically when it finishes.`,
|
||||
"DO NOT sleep, poll for progress, ask the subagent for status, or duplicate this subagent's work; avoid working with the same files or topics it is using.",
|
||||
"Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.",
|
||||
].join("\n")
|
||||
].join("\n"),
|
||||
})
|
||||
|
||||
export const Input = Schema.Struct({
|
||||
agent: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
|
||||
@@ -259,11 +262,7 @@ export const Plugin = {
|
||||
if (background) {
|
||||
yield* runtime.job.background(info.id)
|
||||
yield* notifyWhenDone(context.sessionID, child.id, agent.name, input.description, info.started_at)
|
||||
return {
|
||||
sessionID: child.id,
|
||||
status: "running" as const,
|
||||
output: backgroundStarted(child.id),
|
||||
}
|
||||
return backgroundResult(child.id)
|
||||
}
|
||||
|
||||
const result = yield* runtime.job.block({ id: child.id, sessionID: context.sessionID }).pipe(
|
||||
@@ -281,11 +280,7 @@ export const Plugin = {
|
||||
input.description,
|
||||
result.info.started_at,
|
||||
)
|
||||
return {
|
||||
sessionID: child.id,
|
||||
status: "running" as const,
|
||||
output: backgroundStarted(child.id),
|
||||
}
|
||||
return backgroundResult(child.id)
|
||||
}
|
||||
// Failure surfaces keep the sessionID visible so the model can continue the child.
|
||||
if (result?.info.status === "error")
|
||||
|
||||
@@ -47,7 +47,6 @@ const acceptHeader = (format: Format) => {
|
||||
case "html":
|
||||
return "text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1"
|
||||
}
|
||||
return "*/*"
|
||||
}
|
||||
|
||||
const headers = (format: Format, userAgent: string) => ({
|
||||
|
||||
@@ -220,8 +220,6 @@ const layer = Layer.effect(
|
||||
const gitStrategy = yield* WorktreeGit.make
|
||||
yield* register(gitStrategy).pipe(Effect.orDie)
|
||||
|
||||
const strategies = () => Array.from(registry.values())
|
||||
|
||||
const source = Effect.fnUntraced(function* (input: AbsolutePath | undefined, projectID: ProjectSchema.ID) {
|
||||
const sourceDirectory = input ?? (yield* ops.primary(projectID))?.directory
|
||||
if (!sourceDirectory) return yield* new SourceDirectoryNotFoundError({ projectID })
|
||||
@@ -290,7 +288,7 @@ const layer = Layer.effect(
|
||||
const discovered = yield* Effect.forEach(
|
||||
sourceDirectories,
|
||||
(sourceDirectory) =>
|
||||
Effect.forEach(strategies(), (strategy) =>
|
||||
Effect.forEach(Array.from(registry.values()), (strategy) =>
|
||||
strategy.list(sourceDirectory).pipe(
|
||||
Effect.catchTag("Worktree.DirectoryUnavailableError", () => Effect.succeed([])),
|
||||
Effect.map((items) =>
|
||||
|
||||
Reference in New Issue
Block a user