mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 19:09:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87480f598f |
+1514
-1516
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
export * as ACP from "./agent"
|
||||||
@@ -24,389 +24,387 @@ import { InstanceState } from "@/effect"
|
|||||||
import * as Option from "effect/Option"
|
import * as Option from "effect/Option"
|
||||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||||
|
|
||||||
export namespace Agent {
|
export const Info = z
|
||||||
export const Info = z
|
.object({
|
||||||
.object({
|
name: z.string(),
|
||||||
name: z.string(),
|
description: z.string().optional(),
|
||||||
description: z.string().optional(),
|
mode: z.enum(["subagent", "primary", "all"]),
|
||||||
mode: z.enum(["subagent", "primary", "all"]),
|
native: z.boolean().optional(),
|
||||||
native: z.boolean().optional(),
|
hidden: z.boolean().optional(),
|
||||||
hidden: z.boolean().optional(),
|
topP: z.number().optional(),
|
||||||
topP: z.number().optional(),
|
temperature: z.number().optional(),
|
||||||
temperature: z.number().optional(),
|
color: z.string().optional(),
|
||||||
color: z.string().optional(),
|
permission: Permission.Ruleset.zod,
|
||||||
permission: Permission.Ruleset.zod,
|
model: z
|
||||||
model: z
|
.object({
|
||||||
.object({
|
modelID: ModelID.zod,
|
||||||
modelID: ModelID.zod,
|
providerID: ProviderID.zod,
|
||||||
providerID: ProviderID.zod,
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
variant: z.string().optional(),
|
|
||||||
prompt: z.string().optional(),
|
|
||||||
options: z.record(z.string(), z.any()),
|
|
||||||
steps: z.number().int().positive().optional(),
|
|
||||||
})
|
|
||||||
.meta({
|
|
||||||
ref: "Agent",
|
|
||||||
})
|
|
||||||
export type Info = z.infer<typeof Info>
|
|
||||||
|
|
||||||
export interface Interface {
|
|
||||||
readonly get: (agent: string) => Effect.Effect<Agent.Info>
|
|
||||||
readonly list: () => Effect.Effect<Agent.Info[]>
|
|
||||||
readonly defaultAgent: () => Effect.Effect<string>
|
|
||||||
readonly generate: (input: {
|
|
||||||
description: string
|
|
||||||
model?: { providerID: ProviderID; modelID: ModelID }
|
|
||||||
}) => Effect.Effect<{
|
|
||||||
identifier: string
|
|
||||||
whenToUse: string
|
|
||||||
systemPrompt: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
type State = Omit<Interface, "generate">
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
|
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const config = yield* Config.Service
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const skill = yield* Skill.Service
|
|
||||||
const provider = yield* Provider.Service
|
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
|
||||||
Effect.fn("Agent.state")(function* (_ctx) {
|
|
||||||
const cfg = yield* config.get()
|
|
||||||
const skillDirs = yield* skill.dirs()
|
|
||||||
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
|
||||||
|
|
||||||
const defaults = Permission.fromConfig({
|
|
||||||
"*": "allow",
|
|
||||||
doom_loop: "ask",
|
|
||||||
external_directory: {
|
|
||||||
"*": "ask",
|
|
||||||
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
|
||||||
},
|
|
||||||
question: "deny",
|
|
||||||
plan_enter: "deny",
|
|
||||||
plan_exit: "deny",
|
|
||||||
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
|
||||||
read: {
|
|
||||||
"*": "allow",
|
|
||||||
"*.env": "ask",
|
|
||||||
"*.env.*": "ask",
|
|
||||||
"*.env.example": "allow",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const user = Permission.fromConfig(cfg.permission ?? {})
|
|
||||||
|
|
||||||
const agents: Record<string, Info> = {
|
|
||||||
build: {
|
|
||||||
name: "build",
|
|
||||||
description: "The default agent. Executes tools based on configured permissions.",
|
|
||||||
options: {},
|
|
||||||
permission: Permission.merge(
|
|
||||||
defaults,
|
|
||||||
Permission.fromConfig({
|
|
||||||
question: "allow",
|
|
||||||
plan_enter: "allow",
|
|
||||||
}),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
mode: "primary",
|
|
||||||
native: true,
|
|
||||||
},
|
|
||||||
plan: {
|
|
||||||
name: "plan",
|
|
||||||
description: "Plan mode. Disallows all edit tools.",
|
|
||||||
options: {},
|
|
||||||
permission: Permission.merge(
|
|
||||||
defaults,
|
|
||||||
Permission.fromConfig({
|
|
||||||
question: "allow",
|
|
||||||
plan_exit: "allow",
|
|
||||||
external_directory: {
|
|
||||||
[path.join(Global.Path.data, "plans", "*")]: "allow",
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
"*": "deny",
|
|
||||||
[path.join(".opencode", "plans", "*.md")]: "allow",
|
|
||||||
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]:
|
|
||||||
"allow",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
mode: "primary",
|
|
||||||
native: true,
|
|
||||||
},
|
|
||||||
general: {
|
|
||||||
name: "general",
|
|
||||||
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
|
|
||||||
permission: Permission.merge(
|
|
||||||
defaults,
|
|
||||||
Permission.fromConfig({
|
|
||||||
todowrite: "deny",
|
|
||||||
}),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
options: {},
|
|
||||||
mode: "subagent",
|
|
||||||
native: true,
|
|
||||||
},
|
|
||||||
explore: {
|
|
||||||
name: "explore",
|
|
||||||
permission: Permission.merge(
|
|
||||||
defaults,
|
|
||||||
Permission.fromConfig({
|
|
||||||
"*": "deny",
|
|
||||||
grep: "allow",
|
|
||||||
glob: "allow",
|
|
||||||
list: "allow",
|
|
||||||
bash: "allow",
|
|
||||||
webfetch: "allow",
|
|
||||||
websearch: "allow",
|
|
||||||
codesearch: "allow",
|
|
||||||
read: "allow",
|
|
||||||
external_directory: {
|
|
||||||
"*": "ask",
|
|
||||||
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
|
|
||||||
prompt: PROMPT_EXPLORE,
|
|
||||||
options: {},
|
|
||||||
mode: "subagent",
|
|
||||||
native: true,
|
|
||||||
},
|
|
||||||
compaction: {
|
|
||||||
name: "compaction",
|
|
||||||
mode: "primary",
|
|
||||||
native: true,
|
|
||||||
hidden: true,
|
|
||||||
prompt: PROMPT_COMPACTION,
|
|
||||||
permission: Permission.merge(
|
|
||||||
defaults,
|
|
||||||
Permission.fromConfig({
|
|
||||||
"*": "deny",
|
|
||||||
}),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
name: "title",
|
|
||||||
mode: "primary",
|
|
||||||
options: {},
|
|
||||||
native: true,
|
|
||||||
hidden: true,
|
|
||||||
temperature: 0.5,
|
|
||||||
permission: Permission.merge(
|
|
||||||
defaults,
|
|
||||||
Permission.fromConfig({
|
|
||||||
"*": "deny",
|
|
||||||
}),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
prompt: PROMPT_TITLE,
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
name: "summary",
|
|
||||||
mode: "primary",
|
|
||||||
options: {},
|
|
||||||
native: true,
|
|
||||||
hidden: true,
|
|
||||||
permission: Permission.merge(
|
|
||||||
defaults,
|
|
||||||
Permission.fromConfig({
|
|
||||||
"*": "deny",
|
|
||||||
}),
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
prompt: PROMPT_SUMMARY,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
|
||||||
if (value.disable) {
|
|
||||||
delete agents[key]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let item = agents[key]
|
|
||||||
if (!item)
|
|
||||||
item = agents[key] = {
|
|
||||||
name: key,
|
|
||||||
mode: "all",
|
|
||||||
permission: Permission.merge(defaults, user),
|
|
||||||
options: {},
|
|
||||||
native: false,
|
|
||||||
}
|
|
||||||
if (value.model) item.model = Provider.parseModel(value.model)
|
|
||||||
item.variant = value.variant ?? item.variant
|
|
||||||
item.prompt = value.prompt ?? item.prompt
|
|
||||||
item.description = value.description ?? item.description
|
|
||||||
item.temperature = value.temperature ?? item.temperature
|
|
||||||
item.topP = value.top_p ?? item.topP
|
|
||||||
item.mode = value.mode ?? item.mode
|
|
||||||
item.color = value.color ?? item.color
|
|
||||||
item.hidden = value.hidden ?? item.hidden
|
|
||||||
item.name = value.name ?? item.name
|
|
||||||
item.steps = value.steps ?? item.steps
|
|
||||||
item.options = mergeDeep(item.options, value.options ?? {})
|
|
||||||
item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Truncate.GLOB is allowed unless explicitly configured
|
|
||||||
for (const name in agents) {
|
|
||||||
const agent = agents[name]
|
|
||||||
const explicit = agent.permission.some((r) => {
|
|
||||||
if (r.permission !== "external_directory") return false
|
|
||||||
if (r.action !== "deny") return false
|
|
||||||
return r.pattern === Truncate.GLOB
|
|
||||||
})
|
|
||||||
if (explicit) continue
|
|
||||||
|
|
||||||
agents[name].permission = Permission.merge(
|
|
||||||
agents[name].permission,
|
|
||||||
Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = Effect.fnUntraced(function* (agent: string) {
|
|
||||||
return agents[agent]
|
|
||||||
})
|
|
||||||
|
|
||||||
const list = Effect.fnUntraced(function* () {
|
|
||||||
const cfg = yield* config.get()
|
|
||||||
return pipe(
|
|
||||||
agents,
|
|
||||||
values(),
|
|
||||||
sortBy(
|
|
||||||
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
|
|
||||||
[(x) => x.name, "asc"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const defaultAgent = Effect.fnUntraced(function* () {
|
|
||||||
const c = yield* config.get()
|
|
||||||
if (c.default_agent) {
|
|
||||||
const agent = agents[c.default_agent]
|
|
||||||
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
|
|
||||||
if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
|
|
||||||
if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
|
|
||||||
return agent.name
|
|
||||||
}
|
|
||||||
const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
|
|
||||||
if (!visible) throw new Error("no primary visible agent found")
|
|
||||||
return visible.name
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
get,
|
|
||||||
list,
|
|
||||||
defaultAgent,
|
|
||||||
} satisfies State
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return Service.of({
|
|
||||||
get: Effect.fn("Agent.get")(function* (agent: string) {
|
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.get(agent))
|
|
||||||
}),
|
|
||||||
list: Effect.fn("Agent.list")(function* () {
|
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.list())
|
|
||||||
}),
|
|
||||||
defaultAgent: Effect.fn("Agent.defaultAgent")(function* () {
|
|
||||||
return yield* InstanceState.useEffect(state, (s) => s.defaultAgent())
|
|
||||||
}),
|
|
||||||
generate: Effect.fn("Agent.generate")(function* (input: {
|
|
||||||
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 tracer = cfg.experimental?.openTelemetry
|
|
||||||
? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer))
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const system = [PROMPT_GENERATE]
|
|
||||||
yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
|
|
||||||
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
|
|
||||||
|
|
||||||
// TODO: clean this up so provider specific logic doesnt bleed over
|
|
||||||
const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
|
|
||||||
const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth"
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
experimental_telemetry: {
|
|
||||||
isEnabled: cfg.experimental?.openTelemetry,
|
|
||||||
tracer,
|
|
||||||
metadata: {
|
|
||||||
userId: cfg.username ?? "unknown",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
temperature: 0.3,
|
|
||||||
messages: [
|
|
||||||
...(isOpenaiOauth
|
|
||||||
? []
|
|
||||||
: 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]
|
|
||||||
|
|
||||||
if (isOpenaiOauth) {
|
|
||||||
return yield* Effect.promise(async () => {
|
|
||||||
const result = streamObject({
|
|
||||||
...params,
|
|
||||||
providerOptions: ProviderTransform.providerOptions(resolved, {
|
|
||||||
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 yield* Effect.promise(() => generateObject(params).then((r) => r.object))
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}),
|
.optional(),
|
||||||
)
|
variant: z.string().optional(),
|
||||||
|
prompt: z.string().optional(),
|
||||||
|
options: z.record(z.string(), z.any()),
|
||||||
|
steps: z.number().int().positive().optional(),
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
ref: "Agent",
|
||||||
|
})
|
||||||
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export interface Interface {
|
||||||
Layer.provide(Plugin.defaultLayer),
|
readonly get: (agent: string) => Effect.Effect<Info>
|
||||||
Layer.provide(Provider.defaultLayer),
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
Layer.provide(Auth.defaultLayer),
|
readonly defaultAgent: () => Effect.Effect<string>
|
||||||
Layer.provide(Config.defaultLayer),
|
readonly generate: (input: {
|
||||||
Layer.provide(Skill.defaultLayer),
|
description: string
|
||||||
)
|
model?: { providerID: ProviderID; modelID: ModelID }
|
||||||
|
}) => Effect.Effect<{
|
||||||
|
identifier: string
|
||||||
|
whenToUse: string
|
||||||
|
systemPrompt: string
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type State = Omit<Interface, "generate">
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const config = yield* Config.Service
|
||||||
|
const auth = yield* Auth.Service
|
||||||
|
const plugin = yield* Plugin.Service
|
||||||
|
const skill = yield* Skill.Service
|
||||||
|
const provider = yield* Provider.Service
|
||||||
|
|
||||||
|
const state = yield* InstanceState.make<State>(
|
||||||
|
Effect.fn("Agent.state")(function* (_ctx) {
|
||||||
|
const cfg = yield* config.get()
|
||||||
|
const skillDirs = yield* skill.dirs()
|
||||||
|
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
||||||
|
|
||||||
|
const defaults = Permission.fromConfig({
|
||||||
|
"*": "allow",
|
||||||
|
doom_loop: "ask",
|
||||||
|
external_directory: {
|
||||||
|
"*": "ask",
|
||||||
|
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
||||||
|
},
|
||||||
|
question: "deny",
|
||||||
|
plan_enter: "deny",
|
||||||
|
plan_exit: "deny",
|
||||||
|
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
||||||
|
read: {
|
||||||
|
"*": "allow",
|
||||||
|
"*.env": "ask",
|
||||||
|
"*.env.*": "ask",
|
||||||
|
"*.env.example": "allow",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = Permission.fromConfig(cfg.permission ?? {})
|
||||||
|
|
||||||
|
const agents: Record<string, Info> = {
|
||||||
|
build: {
|
||||||
|
name: "build",
|
||||||
|
description: "The default agent. Executes tools based on configured permissions.",
|
||||||
|
options: {},
|
||||||
|
permission: Permission.merge(
|
||||||
|
defaults,
|
||||||
|
Permission.fromConfig({
|
||||||
|
question: "allow",
|
||||||
|
plan_enter: "allow",
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
mode: "primary",
|
||||||
|
native: true,
|
||||||
|
},
|
||||||
|
plan: {
|
||||||
|
name: "plan",
|
||||||
|
description: "Plan mode. Disallows all edit tools.",
|
||||||
|
options: {},
|
||||||
|
permission: Permission.merge(
|
||||||
|
defaults,
|
||||||
|
Permission.fromConfig({
|
||||||
|
question: "allow",
|
||||||
|
plan_exit: "allow",
|
||||||
|
external_directory: {
|
||||||
|
[path.join(Global.Path.data, "plans", "*")]: "allow",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
"*": "deny",
|
||||||
|
[path.join(".opencode", "plans", "*.md")]: "allow",
|
||||||
|
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]:
|
||||||
|
"allow",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
mode: "primary",
|
||||||
|
native: true,
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
name: "general",
|
||||||
|
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
|
||||||
|
permission: Permission.merge(
|
||||||
|
defaults,
|
||||||
|
Permission.fromConfig({
|
||||||
|
todowrite: "deny",
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
options: {},
|
||||||
|
mode: "subagent",
|
||||||
|
native: true,
|
||||||
|
},
|
||||||
|
explore: {
|
||||||
|
name: "explore",
|
||||||
|
permission: Permission.merge(
|
||||||
|
defaults,
|
||||||
|
Permission.fromConfig({
|
||||||
|
"*": "deny",
|
||||||
|
grep: "allow",
|
||||||
|
glob: "allow",
|
||||||
|
list: "allow",
|
||||||
|
bash: "allow",
|
||||||
|
webfetch: "allow",
|
||||||
|
websearch: "allow",
|
||||||
|
codesearch: "allow",
|
||||||
|
read: "allow",
|
||||||
|
external_directory: {
|
||||||
|
"*": "ask",
|
||||||
|
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
|
||||||
|
prompt: PROMPT_EXPLORE,
|
||||||
|
options: {},
|
||||||
|
mode: "subagent",
|
||||||
|
native: true,
|
||||||
|
},
|
||||||
|
compaction: {
|
||||||
|
name: "compaction",
|
||||||
|
mode: "primary",
|
||||||
|
native: true,
|
||||||
|
hidden: true,
|
||||||
|
prompt: PROMPT_COMPACTION,
|
||||||
|
permission: Permission.merge(
|
||||||
|
defaults,
|
||||||
|
Permission.fromConfig({
|
||||||
|
"*": "deny",
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
name: "title",
|
||||||
|
mode: "primary",
|
||||||
|
options: {},
|
||||||
|
native: true,
|
||||||
|
hidden: true,
|
||||||
|
temperature: 0.5,
|
||||||
|
permission: Permission.merge(
|
||||||
|
defaults,
|
||||||
|
Permission.fromConfig({
|
||||||
|
"*": "deny",
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
prompt: PROMPT_TITLE,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
name: "summary",
|
||||||
|
mode: "primary",
|
||||||
|
options: {},
|
||||||
|
native: true,
|
||||||
|
hidden: true,
|
||||||
|
permission: Permission.merge(
|
||||||
|
defaults,
|
||||||
|
Permission.fromConfig({
|
||||||
|
"*": "deny",
|
||||||
|
}),
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
prompt: PROMPT_SUMMARY,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||||
|
if (value.disable) {
|
||||||
|
delete agents[key]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let item = agents[key]
|
||||||
|
if (!item)
|
||||||
|
item = agents[key] = {
|
||||||
|
name: key,
|
||||||
|
mode: "all",
|
||||||
|
permission: Permission.merge(defaults, user),
|
||||||
|
options: {},
|
||||||
|
native: false,
|
||||||
|
}
|
||||||
|
if (value.model) item.model = Provider.parseModel(value.model)
|
||||||
|
item.variant = value.variant ?? item.variant
|
||||||
|
item.prompt = value.prompt ?? item.prompt
|
||||||
|
item.description = value.description ?? item.description
|
||||||
|
item.temperature = value.temperature ?? item.temperature
|
||||||
|
item.topP = value.top_p ?? item.topP
|
||||||
|
item.mode = value.mode ?? item.mode
|
||||||
|
item.color = value.color ?? item.color
|
||||||
|
item.hidden = value.hidden ?? item.hidden
|
||||||
|
item.name = value.name ?? item.name
|
||||||
|
item.steps = value.steps ?? item.steps
|
||||||
|
item.options = mergeDeep(item.options, value.options ?? {})
|
||||||
|
item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Truncate.GLOB is allowed unless explicitly configured
|
||||||
|
for (const name in agents) {
|
||||||
|
const agent = agents[name]
|
||||||
|
const explicit = agent.permission.some((r) => {
|
||||||
|
if (r.permission !== "external_directory") return false
|
||||||
|
if (r.action !== "deny") return false
|
||||||
|
return r.pattern === Truncate.GLOB
|
||||||
|
})
|
||||||
|
if (explicit) continue
|
||||||
|
|
||||||
|
agents[name].permission = Permission.merge(
|
||||||
|
agents[name].permission,
|
||||||
|
Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const get = Effect.fnUntraced(function* (agent: string) {
|
||||||
|
return agents[agent]
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = Effect.fnUntraced(function* () {
|
||||||
|
const cfg = yield* config.get()
|
||||||
|
return pipe(
|
||||||
|
agents,
|
||||||
|
values(),
|
||||||
|
sortBy(
|
||||||
|
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
|
||||||
|
[(x) => x.name, "asc"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultAgent = Effect.fnUntraced(function* () {
|
||||||
|
const c = yield* config.get()
|
||||||
|
if (c.default_agent) {
|
||||||
|
const agent = agents[c.default_agent]
|
||||||
|
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
|
||||||
|
if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
|
||||||
|
if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
|
||||||
|
return agent.name
|
||||||
|
}
|
||||||
|
const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
|
||||||
|
if (!visible) throw new Error("no primary visible agent found")
|
||||||
|
return visible.name
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
get,
|
||||||
|
list,
|
||||||
|
defaultAgent,
|
||||||
|
} satisfies State
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Service.of({
|
||||||
|
get: Effect.fn("Agent.get")(function* (agent: string) {
|
||||||
|
return yield* InstanceState.useEffect(state, (s) => s.get(agent))
|
||||||
|
}),
|
||||||
|
list: Effect.fn("Agent.list")(function* () {
|
||||||
|
return yield* InstanceState.useEffect(state, (s) => s.list())
|
||||||
|
}),
|
||||||
|
defaultAgent: Effect.fn("Agent.defaultAgent")(function* () {
|
||||||
|
return yield* InstanceState.useEffect(state, (s) => s.defaultAgent())
|
||||||
|
}),
|
||||||
|
generate: Effect.fn("Agent.generate")(function* (input: {
|
||||||
|
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 tracer = cfg.experimental?.openTelemetry
|
||||||
|
? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const system = [PROMPT_GENERATE]
|
||||||
|
yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
|
||||||
|
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
|
||||||
|
|
||||||
|
// TODO: clean this up so provider specific logic doesnt bleed over
|
||||||
|
const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
|
||||||
|
const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth"
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
experimental_telemetry: {
|
||||||
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
|
tracer,
|
||||||
|
metadata: {
|
||||||
|
userId: cfg.username ?? "unknown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
temperature: 0.3,
|
||||||
|
messages: [
|
||||||
|
...(isOpenaiOauth
|
||||||
|
? []
|
||||||
|
: 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]
|
||||||
|
|
||||||
|
if (isOpenaiOauth) {
|
||||||
|
return yield* Effect.promise(async () => {
|
||||||
|
const result = streamObject({
|
||||||
|
...params,
|
||||||
|
providerOptions: ProviderTransform.providerOptions(resolved, {
|
||||||
|
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 yield* Effect.promise(() => generateObject(params).then((r) => r.object))
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(
|
||||||
|
Layer.provide(Plugin.defaultLayer),
|
||||||
|
Layer.provide(Provider.defaultLayer),
|
||||||
|
Layer.provide(Auth.defaultLayer),
|
||||||
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(Skill.defaultLayer),
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * as Agent from "./agent"
|
||||||
@@ -2,7 +2,7 @@ import { Log } from "@/util"
|
|||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||||
import { ACP } from "@/acp/agent"
|
import { ACP } from "@/acp"
|
||||||
import { Server } from "@/server/server"
|
import { Server } from "@/server/server"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as prompts from "@clack/prompts"
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent"
|
||||||
import { Provider } from "../../provider"
|
import { Provider } from "../../provider"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { basename } from "path"
|
import { basename } from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Agent } from "../../../agent/agent"
|
import { Agent } from "../../../agent"
|
||||||
import { Provider } from "../../../provider"
|
import { Provider } from "../../../provider"
|
||||||
import { Session } from "../../../session"
|
import { Session } from "../../../session"
|
||||||
import type { MessageV2 } from "../../../session/message-v2"
|
import type { MessageV2 } from "../../../session/message-v2"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Filesystem } from "../../util"
|
|||||||
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { Provider } from "../../provider"
|
import { Provider } from "../../provider"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent"
|
||||||
import { Permission } from "../../permission"
|
import { Permission } from "../../permission"
|
||||||
import { Tool } from "../../tool"
|
import { Tool } from "../../tool"
|
||||||
import { GlobTool } from "../../tool/glob"
|
import { GlobTool } from "../../tool/glob"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * as Workspace from "./workspace"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { ZodOverride } from "@/util/effect-zod"
|
import { ZodOverride } from "@/util/effect-zod"
|
||||||
import { withStatics } from "@/util/schema"
|
import { withStatics } from "@/util/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -25,168 +25,234 @@ import { AppRuntime } from "@/effect/app-runtime"
|
|||||||
import { EventSequenceTable } from "@/sync/event.sql"
|
import { EventSequenceTable } from "@/sync/event.sql"
|
||||||
import { waitEvent } from "./util"
|
import { waitEvent } from "./util"
|
||||||
|
|
||||||
export namespace Workspace {
|
export const Info = WorkspaceInfo.meta({
|
||||||
export const Info = WorkspaceInfo.meta({
|
ref: "Workspace",
|
||||||
ref: "Workspace",
|
})
|
||||||
})
|
export type Info = z.infer<typeof Info>
|
||||||
export type Info = z.infer<typeof Info>
|
|
||||||
|
|
||||||
export const ConnectionStatus = z.object({
|
export const ConnectionStatus = z.object({
|
||||||
workspaceID: WorkspaceID.zod,
|
workspaceID: WorkspaceID.zod,
|
||||||
status: z.enum(["connected", "connecting", "disconnected", "error"]),
|
status: z.enum(["connected", "connecting", "disconnected", "error"]),
|
||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
})
|
})
|
||||||
export type ConnectionStatus = z.infer<typeof ConnectionStatus>
|
export type ConnectionStatus = z.infer<typeof ConnectionStatus>
|
||||||
|
|
||||||
const Restore = z.object({
|
const Restore = z.object({
|
||||||
workspaceID: WorkspaceID.zod,
|
workspaceID: WorkspaceID.zod,
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID.zod,
|
||||||
total: z.number().int().min(0),
|
total: z.number().int().min(0),
|
||||||
step: z.number().int().min(0),
|
step: z.number().int().min(0),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Ready: BusEvent.define(
|
Ready: BusEvent.define(
|
||||||
"workspace.ready",
|
"workspace.ready",
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Failed: BusEvent.define(
|
Failed: BusEvent.define(
|
||||||
"workspace.failed",
|
"workspace.failed",
|
||||||
z.object({
|
z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Restore: BusEvent.define("workspace.restore", Restore),
|
Restore: BusEvent.define("workspace.restore", Restore),
|
||||||
Status: BusEvent.define("workspace.status", ConnectionStatus),
|
Status: BusEvent.define("workspace.status", ConnectionStatus),
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
type: row.type,
|
||||||
|
branch: row.branch,
|
||||||
|
name: row.name,
|
||||||
|
directory: row.directory,
|
||||||
|
extra: row.extra,
|
||||||
|
projectID: row.project_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateInput = z.object({
|
||||||
|
id: WorkspaceID.zod.optional(),
|
||||||
|
type: Info.shape.type,
|
||||||
|
branch: Info.shape.branch,
|
||||||
|
projectID: ProjectID.zod,
|
||||||
|
extra: Info.shape.extra,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const create = fn(CreateInput, async (input) => {
|
||||||
|
const id = WorkspaceID.ascending(input.id)
|
||||||
|
const adaptor = await getAdaptor(input.projectID, input.type)
|
||||||
|
|
||||||
|
const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null })
|
||||||
|
|
||||||
|
const info: Info = {
|
||||||
|
id,
|
||||||
|
type: config.type,
|
||||||
|
branch: config.branch ?? null,
|
||||||
|
name: config.name ?? null,
|
||||||
|
directory: config.directory ?? null,
|
||||||
|
extra: config.extra ?? null,
|
||||||
|
projectID: input.projectID,
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
Database.use((db) => {
|
||||||
return {
|
db.insert(WorkspaceTable)
|
||||||
id: row.id,
|
.values({
|
||||||
type: row.type,
|
id: info.id,
|
||||||
branch: row.branch,
|
type: info.type,
|
||||||
name: row.name,
|
branch: info.branch,
|
||||||
directory: row.directory,
|
name: info.name,
|
||||||
extra: row.extra,
|
directory: info.directory,
|
||||||
projectID: row.project_id,
|
extra: info.extra,
|
||||||
}
|
project_id: info.projectID,
|
||||||
}
|
})
|
||||||
|
.run()
|
||||||
const CreateInput = z.object({
|
|
||||||
id: WorkspaceID.zod.optional(),
|
|
||||||
type: Info.shape.type,
|
|
||||||
branch: Info.shape.branch,
|
|
||||||
projectID: ProjectID.zod,
|
|
||||||
extra: Info.shape.extra,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const create = fn(CreateInput, async (input) => {
|
await adaptor.create(config)
|
||||||
const id = WorkspaceID.ascending(input.id)
|
|
||||||
const adaptor = await getAdaptor(input.projectID, input.type)
|
|
||||||
|
|
||||||
const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null })
|
startSync(info)
|
||||||
|
|
||||||
const info: Info = {
|
await waitEvent({
|
||||||
id,
|
timeout: TIMEOUT,
|
||||||
type: config.type,
|
fn(event) {
|
||||||
branch: config.branch ?? null,
|
if (event.workspace === info.id && event.payload.type === Event.Status.type) {
|
||||||
name: config.name ?? null,
|
const { status } = event.payload.properties
|
||||||
directory: config.directory ?? null,
|
return status === "error" || status === "connected"
|
||||||
extra: config.extra ?? null,
|
}
|
||||||
projectID: input.projectID,
|
return false
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
|
||||||
Database.use((db) => {
|
return info
|
||||||
db.insert(WorkspaceTable)
|
})
|
||||||
.values({
|
|
||||||
id: info.id,
|
|
||||||
type: info.type,
|
|
||||||
branch: info.branch,
|
|
||||||
name: info.name,
|
|
||||||
directory: info.directory,
|
|
||||||
extra: info.extra,
|
|
||||||
project_id: info.projectID,
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
})
|
|
||||||
|
|
||||||
await adaptor.create(config)
|
const SessionRestoreInput = z.object({
|
||||||
|
workspaceID: WorkspaceID.zod,
|
||||||
|
sessionID: SessionID.zod,
|
||||||
|
})
|
||||||
|
|
||||||
startSync(info)
|
export const sessionRestore = fn(SessionRestoreInput, async (input) => {
|
||||||
|
log.info("session restore requested", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const space = await get(input.workspaceID)
|
||||||
|
if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`)
|
||||||
|
|
||||||
await waitEvent({
|
const adaptor = await getAdaptor(space.projectID, space.type)
|
||||||
timeout: TIMEOUT,
|
const target = await adaptor.target(space)
|
||||||
fn(event) {
|
|
||||||
if (event.workspace === info.id && event.payload.type === Event.Status.type) {
|
// Need to switch the workspace of the session
|
||||||
const { status } = event.payload.properties
|
SyncEvent.run(Session.Event.Updated, {
|
||||||
return status === "error" || status === "connected"
|
sessionID: input.sessionID,
|
||||||
}
|
info: {
|
||||||
return false
|
workspaceID: input.workspaceID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return info
|
const rows = Database.use((db) =>
|
||||||
})
|
db
|
||||||
|
.select({
|
||||||
|
id: EventTable.id,
|
||||||
|
aggregateID: EventTable.aggregate_id,
|
||||||
|
seq: EventTable.seq,
|
||||||
|
type: EventTable.type,
|
||||||
|
data: EventTable.data,
|
||||||
|
})
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, input.sessionID))
|
||||||
|
.orderBy(asc(EventTable.seq))
|
||||||
|
.all(),
|
||||||
|
)
|
||||||
|
if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`)
|
||||||
|
|
||||||
const SessionRestoreInput = z.object({
|
const all = rows
|
||||||
workspaceID: WorkspaceID.zod,
|
|
||||||
sessionID: SessionID.zod,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const sessionRestore = fn(SessionRestoreInput, async (input) => {
|
const size = 10
|
||||||
log.info("session restore requested", {
|
const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size))
|
||||||
|
const total = sets.length
|
||||||
|
log.info("session restore prepared", {
|
||||||
workspaceID: input.workspaceID,
|
workspaceID: input.workspaceID,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
|
workspaceType: space.type,
|
||||||
|
directory: space.directory,
|
||||||
|
target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
|
||||||
|
events: all.length,
|
||||||
|
batches: total,
|
||||||
|
first: all[0]?.seq,
|
||||||
|
last: all.at(-1)?.seq,
|
||||||
})
|
})
|
||||||
try {
|
GlobalBus.emit("event", {
|
||||||
const space = await get(input.workspaceID)
|
directory: "global",
|
||||||
if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`)
|
workspace: input.workspaceID,
|
||||||
|
payload: {
|
||||||
const adaptor = await getAdaptor(space.projectID, space.type)
|
type: Event.Restore.type,
|
||||||
const target = await adaptor.target(space)
|
properties: {
|
||||||
|
|
||||||
// Need to switch the workspace of the session
|
|
||||||
SyncEvent.run(Session.Event.Updated, {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
info: {
|
|
||||||
workspaceID: input.workspaceID,
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
total,
|
||||||
|
step: 0,
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
})
|
||||||
const rows = Database.use((db) =>
|
for (const [i, events] of sets.entries()) {
|
||||||
db
|
log.info("session restore batch starting", {
|
||||||
.select({
|
|
||||||
id: EventTable.id,
|
|
||||||
aggregateID: EventTable.aggregate_id,
|
|
||||||
seq: EventTable.seq,
|
|
||||||
type: EventTable.type,
|
|
||||||
data: EventTable.data,
|
|
||||||
})
|
|
||||||
.from(EventTable)
|
|
||||||
.where(eq(EventTable.aggregate_id, input.sessionID))
|
|
||||||
.orderBy(asc(EventTable.seq))
|
|
||||||
.all(),
|
|
||||||
)
|
|
||||||
if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`)
|
|
||||||
|
|
||||||
const all = rows
|
|
||||||
|
|
||||||
const size = 10
|
|
||||||
const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size))
|
|
||||||
const total = sets.length
|
|
||||||
log.info("session restore prepared", {
|
|
||||||
workspaceID: input.workspaceID,
|
workspaceID: input.workspaceID,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
workspaceType: space.type,
|
step: i + 1,
|
||||||
directory: space.directory,
|
total,
|
||||||
|
events: events.length,
|
||||||
|
first: events[0]?.seq,
|
||||||
|
last: events.at(-1)?.seq,
|
||||||
target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
|
target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
|
||||||
events: all.length,
|
|
||||||
batches: total,
|
|
||||||
first: all[0]?.seq,
|
|
||||||
last: all.at(-1)?.seq,
|
|
||||||
})
|
})
|
||||||
|
if (target.type === "local") {
|
||||||
|
SyncEvent.replayAll(events)
|
||||||
|
log.info("session restore batch replayed locally", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
step: i + 1,
|
||||||
|
total,
|
||||||
|
events: events.length,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const url = route(target.url, "/sync/replay")
|
||||||
|
const headers = new Headers(target.headers)
|
||||||
|
headers.set("content-type", "application/json")
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
directory: space.directory ?? "",
|
||||||
|
events,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text()
|
||||||
|
log.error("session restore batch failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
step: i + 1,
|
||||||
|
total,
|
||||||
|
status: res.status,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
throw new Error(
|
||||||
|
`Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
log.info("session restore batch posted", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
step: i + 1,
|
||||||
|
total,
|
||||||
|
status: res.status,
|
||||||
|
})
|
||||||
|
}
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
directory: "global",
|
directory: "global",
|
||||||
workspace: input.workspaceID,
|
workspace: input.workspaceID,
|
||||||
@@ -196,330 +262,262 @@ export namespace Workspace {
|
|||||||
workspaceID: input.workspaceID,
|
workspaceID: input.workspaceID,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
total,
|
total,
|
||||||
step: 0,
|
step: i + 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
for (const [i, events] of sets.entries()) {
|
}
|
||||||
log.info("session restore batch starting", {
|
|
||||||
workspaceID: input.workspaceID,
|
log.info("session restore complete", {
|
||||||
sessionID: input.sessionID,
|
workspaceID: input.workspaceID,
|
||||||
step: i + 1,
|
sessionID: input.sessionID,
|
||||||
total,
|
batches: total,
|
||||||
events: events.length,
|
})
|
||||||
first: events[0]?.seq,
|
|
||||||
last: events.at(-1)?.seq,
|
return {
|
||||||
target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
|
total,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error("session restore failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function list(project: Project.Info) {
|
||||||
|
const rows = Database.use((db) =>
|
||||||
|
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
|
||||||
|
)
|
||||||
|
const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
|
||||||
|
|
||||||
|
for (const space of spaces) startSync(space)
|
||||||
|
return spaces
|
||||||
|
}
|
||||||
|
|
||||||
|
function lookup(id: WorkspaceID) {
|
||||||
|
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||||
|
if (!row) return
|
||||||
|
return fromRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const get = fn(WorkspaceID.zod, async (id) => {
|
||||||
|
const space = lookup(id)
|
||||||
|
if (!space) return
|
||||||
|
startSync(space)
|
||||||
|
return space
|
||||||
|
})
|
||||||
|
|
||||||
|
export const remove = fn(WorkspaceID.zod, async (id) => {
|
||||||
|
const sessions = Database.use((db) =>
|
||||||
|
db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(),
|
||||||
|
)
|
||||||
|
for (const session of sessions) {
|
||||||
|
await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
stopSync(id)
|
||||||
|
|
||||||
|
const info = fromRow(row)
|
||||||
|
try {
|
||||||
|
const adaptor = await getAdaptor(info.projectID, row.type)
|
||||||
|
await adaptor.remove(info)
|
||||||
|
} catch {
|
||||||
|
log.error("adaptor not available when removing workspace", { type: row.type })
|
||||||
|
}
|
||||||
|
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
||||||
|
const aborts = new Map<WorkspaceID, AbortController>()
|
||||||
|
const TIMEOUT = 5000
|
||||||
|
|
||||||
|
function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) {
|
||||||
|
const prev = connections.get(id)
|
||||||
|
if (prev?.status === status && prev?.error === error) return
|
||||||
|
const next = { workspaceID: id, status, error }
|
||||||
|
connections.set(id, next)
|
||||||
|
|
||||||
|
if (status === "error") {
|
||||||
|
aborts.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: "global",
|
||||||
|
workspace: id,
|
||||||
|
payload: {
|
||||||
|
type: Event.Status.type,
|
||||||
|
properties: next,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function status(): ConnectionStatus[] {
|
||||||
|
return [...connections.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function synced(state: Record<string, number>) {
|
||||||
|
const ids = Object.keys(state)
|
||||||
|
if (ids.length === 0) return true
|
||||||
|
|
||||||
|
const done = Object.fromEntries(
|
||||||
|
Database.use((db) =>
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
id: EventSequenceTable.aggregate_id,
|
||||||
|
seq: EventSequenceTable.seq,
|
||||||
})
|
})
|
||||||
if (target.type === "local") {
|
.from(EventSequenceTable)
|
||||||
SyncEvent.replayAll(events)
|
.where(inArray(EventSequenceTable.aggregate_id, ids))
|
||||||
log.info("session restore batch replayed locally", {
|
.all(),
|
||||||
workspaceID: input.workspaceID,
|
).map((row) => [row.id, row.seq]),
|
||||||
sessionID: input.sessionID,
|
) as Record<string, number>
|
||||||
step: i + 1,
|
|
||||||
total,
|
return ids.every((id) => {
|
||||||
events: events.length,
|
return (done[id] ?? -1) >= state[id]
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
const url = route(target.url, "/sync/replay")
|
|
||||||
const headers = new Headers(target.headers)
|
export async function isSyncing(workspaceID: WorkspaceID) {
|
||||||
headers.set("content-type", "application/json")
|
return aborts.has(workspaceID)
|
||||||
const res = await fetch(url, {
|
}
|
||||||
method: "POST",
|
|
||||||
headers,
|
export async function waitForSync(workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) {
|
||||||
body: JSON.stringify({
|
if (synced(state)) return
|
||||||
directory: space.directory ?? "",
|
|
||||||
events,
|
try {
|
||||||
}),
|
await waitEvent({
|
||||||
})
|
timeout: TIMEOUT,
|
||||||
if (!res.ok) {
|
signal,
|
||||||
const body = await res.text()
|
fn(event) {
|
||||||
log.error("session restore batch failed", {
|
if (event.workspace !== workspaceID && event.payload.type !== "sync") {
|
||||||
workspaceID: input.workspaceID,
|
return false
|
||||||
sessionID: input.sessionID,
|
|
||||||
step: i + 1,
|
|
||||||
total,
|
|
||||||
status: res.status,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
throw new Error(
|
|
||||||
`Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
log.info("session restore batch posted", {
|
|
||||||
workspaceID: input.workspaceID,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
step: i + 1,
|
|
||||||
total,
|
|
||||||
status: res.status,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
GlobalBus.emit("event", {
|
return synced(state)
|
||||||
directory: "global",
|
|
||||||
workspace: input.workspaceID,
|
|
||||||
payload: {
|
|
||||||
type: Event.Restore.type,
|
|
||||||
properties: {
|
|
||||||
workspaceID: input.workspaceID,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
total,
|
|
||||||
step: i + 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("session restore complete", {
|
|
||||||
workspaceID: input.workspaceID,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
batches: total,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
total,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
log.error("session restore failed", {
|
|
||||||
workspaceID: input.workspaceID,
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
error: errorData(err),
|
|
||||||
})
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export function list(project: Project.Info) {
|
|
||||||
const rows = Database.use((db) =>
|
|
||||||
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
|
|
||||||
)
|
|
||||||
const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
|
|
||||||
|
|
||||||
for (const space of spaces) startSync(space)
|
|
||||||
return spaces
|
|
||||||
}
|
|
||||||
|
|
||||||
function lookup(id: WorkspaceID) {
|
|
||||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
|
||||||
if (!row) return
|
|
||||||
return fromRow(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const get = fn(WorkspaceID.zod, async (id) => {
|
|
||||||
const space = lookup(id)
|
|
||||||
if (!space) return
|
|
||||||
startSync(space)
|
|
||||||
return space
|
|
||||||
})
|
|
||||||
|
|
||||||
export const remove = fn(WorkspaceID.zod, async (id) => {
|
|
||||||
const sessions = Database.use((db) =>
|
|
||||||
db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(),
|
|
||||||
)
|
|
||||||
for (const session of sessions) {
|
|
||||||
await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
|
||||||
|
|
||||||
if (row) {
|
|
||||||
stopSync(id)
|
|
||||||
|
|
||||||
const info = fromRow(row)
|
|
||||||
try {
|
|
||||||
const adaptor = await getAdaptor(info.projectID, row.type)
|
|
||||||
await adaptor.remove(info)
|
|
||||||
} catch {
|
|
||||||
log.error("adaptor not available when removing workspace", { type: row.type })
|
|
||||||
}
|
|
||||||
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
|
||||||
const aborts = new Map<WorkspaceID, AbortController>()
|
|
||||||
const TIMEOUT = 5000
|
|
||||||
|
|
||||||
function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) {
|
|
||||||
const prev = connections.get(id)
|
|
||||||
if (prev?.status === status && prev?.error === error) return
|
|
||||||
const next = { workspaceID: id, status, error }
|
|
||||||
connections.set(id, next)
|
|
||||||
|
|
||||||
if (status === "error") {
|
|
||||||
aborts.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory: "global",
|
|
||||||
workspace: id,
|
|
||||||
payload: {
|
|
||||||
type: Event.Status.type,
|
|
||||||
properties: next,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} catch {
|
||||||
|
if (signal?.aborted) throw signal.reason ?? new Error("Request aborted")
|
||||||
|
throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function status(): ConnectionStatus[] {
|
const log = Log.create({ service: "workspace-sync" })
|
||||||
return [...connections.values()]
|
|
||||||
}
|
|
||||||
|
|
||||||
function synced(state: Record<string, number>) {
|
function route(url: string | URL, path: string) {
|
||||||
const ids = Object.keys(state)
|
const next = new URL(url)
|
||||||
if (ids.length === 0) return true
|
next.pathname = `${next.pathname.replace(/\/$/, "")}${path}`
|
||||||
|
next.search = ""
|
||||||
|
next.hash = ""
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
const done = Object.fromEntries(
|
async function syncWorkspace(space: Info, signal: AbortSignal) {
|
||||||
Database.use((db) =>
|
while (!signal.aborted) {
|
||||||
db
|
log.info("connecting to global sync", { workspace: space.name })
|
||||||
.select({
|
setStatus(space.id, "connecting")
|
||||||
id: EventSequenceTable.aggregate_id,
|
|
||||||
seq: EventSequenceTable.seq,
|
|
||||||
})
|
|
||||||
.from(EventSequenceTable)
|
|
||||||
.where(inArray(EventSequenceTable.aggregate_id, ids))
|
|
||||||
.all(),
|
|
||||||
).map((row) => [row.id, row.seq]),
|
|
||||||
) as Record<string, number>
|
|
||||||
|
|
||||||
return ids.every((id) => {
|
|
||||||
return (done[id] ?? -1) >= state[id]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isSyncing(workspaceID: WorkspaceID) {
|
|
||||||
return aborts.has(workspaceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForSync(workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) {
|
|
||||||
if (synced(state)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await waitEvent({
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
signal,
|
|
||||||
fn(event) {
|
|
||||||
if (event.workspace !== workspaceID && event.payload.type !== "sync") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return synced(state)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
if (signal?.aborted) throw signal.reason ?? new Error("Request aborted")
|
|
||||||
throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const log = Log.create({ service: "workspace-sync" })
|
|
||||||
|
|
||||||
function route(url: string | URL, path: string) {
|
|
||||||
const next = new URL(url)
|
|
||||||
next.pathname = `${next.pathname.replace(/\/$/, "")}${path}`
|
|
||||||
next.search = ""
|
|
||||||
next.hash = ""
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncWorkspace(space: Info, signal: AbortSignal) {
|
|
||||||
while (!signal.aborted) {
|
|
||||||
log.info("connecting to global sync", { workspace: space.name })
|
|
||||||
setStatus(space.id, "connecting")
|
|
||||||
|
|
||||||
const adaptor = await getAdaptor(space.projectID, space.type)
|
|
||||||
const target = await adaptor.target(space)
|
|
||||||
|
|
||||||
if (target.type === "local") return
|
|
||||||
|
|
||||||
const res = await fetch(route(target.url, "/global/event"), {
|
|
||||||
method: "GET",
|
|
||||||
headers: target.headers,
|
|
||||||
signal,
|
|
||||||
}).catch((err: unknown) => {
|
|
||||||
setStatus(space.id, "error", err instanceof Error ? err.message : String(err))
|
|
||||||
|
|
||||||
log.info("failed to connect to global sync", {
|
|
||||||
workspace: space.name,
|
|
||||||
error: err,
|
|
||||||
})
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res || !res.ok || !res.body) {
|
|
||||||
const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}`
|
|
||||||
log.info("failed to connect to global sync", { workspace: space.name, error })
|
|
||||||
setStatus(space.id, "error", error)
|
|
||||||
await sleep(1000)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("global sync connected", { workspace: space.name })
|
|
||||||
setStatus(space.id, "connected")
|
|
||||||
|
|
||||||
await parseSSE(res.body, signal, (evt: any) => {
|
|
||||||
try {
|
|
||||||
if (!("payload" in evt)) return
|
|
||||||
|
|
||||||
if (evt.payload.type === "sync") {
|
|
||||||
// This name -> type is temporary
|
|
||||||
SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory: evt.directory,
|
|
||||||
project: evt.project,
|
|
||||||
workspace: space.id,
|
|
||||||
payload: evt.payload,
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
log.info("failed to replay global event", {
|
|
||||||
workspaceID: space.id,
|
|
||||||
error: err,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
log.info("disconnected from global sync: " + space.id)
|
|
||||||
setStatus(space.id, "disconnected")
|
|
||||||
|
|
||||||
// TODO: Implement exponential backoff
|
|
||||||
await sleep(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startSync(space: Info) {
|
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
|
||||||
|
|
||||||
const adaptor = await getAdaptor(space.projectID, space.type)
|
const adaptor = await getAdaptor(space.projectID, space.type)
|
||||||
const target = await adaptor.target(space)
|
const target = await adaptor.target(space)
|
||||||
|
|
||||||
if (target.type === "local") {
|
if (target.type === "local") return
|
||||||
void Filesystem.exists(target.directory).then((exists) => {
|
|
||||||
setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
|
const res = await fetch(route(target.url, "/global/event"), {
|
||||||
|
method: "GET",
|
||||||
|
headers: target.headers,
|
||||||
|
signal,
|
||||||
|
}).catch((err: unknown) => {
|
||||||
|
setStatus(space.id, "error", err instanceof Error ? err.message : String(err))
|
||||||
|
|
||||||
|
log.info("failed to connect to global sync", {
|
||||||
|
workspace: space.name,
|
||||||
|
error: err,
|
||||||
})
|
})
|
||||||
return
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res || !res.ok || !res.body) {
|
||||||
|
const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}`
|
||||||
|
log.info("failed to connect to global sync", { workspace: space.name, error })
|
||||||
|
setStatus(space.id, "error", error)
|
||||||
|
await sleep(1000)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aborts.has(space.id)) return true
|
log.info("global sync connected", { workspace: space.name })
|
||||||
|
setStatus(space.id, "connected")
|
||||||
|
|
||||||
|
await parseSSE(res.body, signal, (evt: any) => {
|
||||||
|
try {
|
||||||
|
if (!("payload" in evt)) return
|
||||||
|
|
||||||
|
if (evt.payload.type === "sync") {
|
||||||
|
// This name -> type is temporary
|
||||||
|
SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: evt.directory,
|
||||||
|
project: evt.project,
|
||||||
|
workspace: space.id,
|
||||||
|
payload: evt.payload,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
log.info("failed to replay global event", {
|
||||||
|
workspaceID: space.id,
|
||||||
|
error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info("disconnected from global sync: " + space.id)
|
||||||
setStatus(space.id, "disconnected")
|
setStatus(space.id, "disconnected")
|
||||||
|
|
||||||
const abort = new AbortController()
|
// TODO: Implement exponential backoff
|
||||||
aborts.set(space.id, abort)
|
await sleep(1000)
|
||||||
|
|
||||||
void syncWorkspace(space, abort.signal).catch((error) => {
|
|
||||||
aborts.delete(space.id)
|
|
||||||
|
|
||||||
setStatus(space.id, "error", String(error))
|
|
||||||
log.warn("workspace listener failed", {
|
|
||||||
workspaceID: space.id,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSync(id: WorkspaceID) {
|
|
||||||
aborts.get(id)?.abort()
|
|
||||||
aborts.delete(id)
|
|
||||||
connections.delete(id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startSync(space: Info) {
|
||||||
|
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
||||||
|
|
||||||
|
const adaptor = await getAdaptor(space.projectID, space.type)
|
||||||
|
const target = await adaptor.target(space)
|
||||||
|
|
||||||
|
if (target.type === "local") {
|
||||||
|
void Filesystem.exists(target.directory).then((exists) => {
|
||||||
|
setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aborts.has(space.id)) return true
|
||||||
|
|
||||||
|
setStatus(space.id, "disconnected")
|
||||||
|
|
||||||
|
const abort = new AbortController()
|
||||||
|
aborts.set(space.id, abort)
|
||||||
|
|
||||||
|
void syncWorkspace(space, abort.signal).catch((error) => {
|
||||||
|
aborts.delete(space.id)
|
||||||
|
|
||||||
|
setStatus(space.id, "error", String(error))
|
||||||
|
log.warn("workspace listener failed", {
|
||||||
|
workspaceID: space.id,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSync(id: WorkspaceID) {
|
||||||
|
aborts.get(id)?.abort()
|
||||||
|
aborts.delete(id)
|
||||||
|
connections.delete(id)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { Snapshot } from "@/snapshot"
|
|||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { Provider } from "@/provider"
|
import { Provider } from "@/provider"
|
||||||
import { ProviderAuth } from "@/provider"
|
import { ProviderAuth } from "@/provider"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent"
|
||||||
import { Skill } from "@/skill"
|
import { Skill } from "@/skill"
|
||||||
import { Discovery } from "@/skill/discovery"
|
import { Discovery } from "@/skill/discovery"
|
||||||
import { Question } from "@/question"
|
import { Question } from "@/question"
|
||||||
|
|||||||
@@ -1,86 +1,84 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { randomBytes } from "crypto"
|
import { randomBytes } from "crypto"
|
||||||
|
|
||||||
export namespace Identifier {
|
const prefixes = {
|
||||||
const prefixes = {
|
event: "evt",
|
||||||
event: "evt",
|
session: "ses",
|
||||||
session: "ses",
|
message: "msg",
|
||||||
message: "msg",
|
permission: "per",
|
||||||
permission: "per",
|
question: "que",
|
||||||
question: "que",
|
user: "usr",
|
||||||
user: "usr",
|
part: "prt",
|
||||||
part: "prt",
|
pty: "pty",
|
||||||
pty: "pty",
|
tool: "tool",
|
||||||
tool: "tool",
|
workspace: "wrk",
|
||||||
workspace: "wrk",
|
entry: "ent",
|
||||||
entry: "ent",
|
} as const
|
||||||
} as const
|
|
||||||
|
|
||||||
export function schema(prefix: keyof typeof prefixes) {
|
export function schema(prefix: keyof typeof prefixes) {
|
||||||
return z.string().startsWith(prefixes[prefix])
|
return z.string().startsWith(prefixes[prefix])
|
||||||
}
|
}
|
||||||
|
|
||||||
const LENGTH = 26
|
const LENGTH = 26
|
||||||
|
|
||||||
// State for monotonic ID generation
|
// State for monotonic ID generation
|
||||||
let lastTimestamp = 0
|
let lastTimestamp = 0
|
||||||
let counter = 0
|
let counter = 0
|
||||||
|
|
||||||
export function ascending(prefix: keyof typeof prefixes, given?: string) {
|
export function ascending(prefix: keyof typeof prefixes, given?: string) {
|
||||||
return generateID(prefix, "ascending", given)
|
return generateID(prefix, "ascending", given)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function descending(prefix: keyof typeof prefixes, given?: string) {
|
export function descending(prefix: keyof typeof prefixes, given?: string) {
|
||||||
return generateID(prefix, "descending", given)
|
return generateID(prefix, "descending", given)
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
||||||
if (!given) {
|
if (!given) {
|
||||||
return create(prefixes[prefix], direction)
|
return create(prefixes[prefix], direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!given.startsWith(prefixes[prefix])) {
|
if (!given.startsWith(prefixes[prefix])) {
|
||||||
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||||
}
|
}
|
||||||
return given
|
return given
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomBase62(length: number): string {
|
function randomBase62(length: number): string {
|
||||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
let result = ""
|
let result = ""
|
||||||
const bytes = randomBytes(length)
|
const bytes = randomBytes(length)
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars[bytes[i] % 62]
|
result += chars[bytes[i] % 62]
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
||||||
const currentTimestamp = timestamp ?? Date.now()
|
const currentTimestamp = timestamp ?? Date.now()
|
||||||
|
|
||||||
if (currentTimestamp !== lastTimestamp) {
|
if (currentTimestamp !== lastTimestamp) {
|
||||||
lastTimestamp = currentTimestamp
|
lastTimestamp = currentTimestamp
|
||||||
counter = 0
|
counter = 0
|
||||||
}
|
}
|
||||||
counter++
|
counter++
|
||||||
|
|
||||||
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
|
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
|
||||||
|
|
||||||
now = direction === "descending" ? ~now : now
|
now = direction === "descending" ? ~now : now
|
||||||
|
|
||||||
const timeBytes = Buffer.alloc(6)
|
const timeBytes = Buffer.alloc(6)
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
||||||
}
|
}
|
||||||
|
|
||||||
return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
|
return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
||||||
export function timestamp(id: string): number {
|
export function timestamp(id: string): number {
|
||||||
const prefix = id.split("_")[0]
|
const prefix = id.split("_")[0]
|
||||||
const hex = id.slice(prefix.length + 1, prefix.length + 13)
|
const hex = id.slice(prefix.length + 1, prefix.length + 13)
|
||||||
const encoded = BigInt("0x" + hex)
|
const encoded = BigInt("0x" + hex)
|
||||||
return Number(encoded / BigInt(0x1000))
|
return Number(encoded / BigInt(0x1000))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * as Identifier from "./id"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { ZodOverride } from "@/util/effect-zod"
|
import { ZodOverride } from "@/util/effect-zod"
|
||||||
import { Newtype } from "@/util/schema"
|
import { Newtype } from "@/util/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { ZodOverride } from "@/util/effect-zod"
|
import { ZodOverride } from "@/util/effect-zod"
|
||||||
import { withStatics } from "@/util/schema"
|
import { withStatics } from "@/util/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Proc } from "#pty"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Log } from "../util"
|
import { Log } from "../util"
|
||||||
import { lazy } from "@opencode-ai/shared/util/lazy"
|
import { lazy } from "@opencode-ai/shared/util/lazy"
|
||||||
import { Shell } from "@/shell/shell"
|
import { Shell } from "@/shell"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { PtyID } from "./schema"
|
import { PtyID } from "./schema"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { ZodOverride } from "@/util/effect-zod"
|
import { ZodOverride } from "@/util/effect-zod"
|
||||||
import { Newtype } from "@/util/schema"
|
import { Newtype } from "@/util/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { MiddlewareHandler } from "hono"
|
import type { MiddlewareHandler } from "hono"
|
||||||
import { Database, inArray } from "@/storage"
|
import { Database, inArray } from "@/storage"
|
||||||
import { EventSequenceTable } from "@/sync/event.sql"
|
import { EventSequenceTable } from "@/sync/event.sql"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane"
|
||||||
import type { WorkspaceID } from "@/control-plane/schema"
|
import type { WorkspaceID } from "@/control-plane/schema"
|
||||||
import { Log } from "@/util"
|
import { Log } from "@/util"
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { errors } from "../error"
|
|||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { Effect, Option } from "effect"
|
import { Effect, Option } from "effect"
|
||||||
import { WorkspaceRoutes } from "./workspace"
|
import { WorkspaceRoutes } from "./workspace"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent"
|
||||||
|
|
||||||
const ConsoleOrgOption = z.object({
|
const ConsoleOrgOption = z.object({
|
||||||
accountID: z.string(),
|
accountID: z.string(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Format } from "../../format"
|
|||||||
import { TuiRoutes } from "./tui"
|
import { TuiRoutes } from "./tui"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { Vcs } from "../../project"
|
import { Vcs } from "../../project"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent"
|
||||||
import { Skill } from "../../skill"
|
import { Skill } from "../../skill"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { LSP } from "../../lsp"
|
import { LSP } from "../../lsp"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { MiddlewareHandler } from "hono"
|
|||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import { getAdaptor } from "@/control-plane/adaptors"
|
import { getAdaptor } from "@/control-plane/adaptors"
|
||||||
import { WorkspaceID } from "@/control-plane/schema"
|
import { WorkspaceID } from "@/control-plane/schema"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane"
|
||||||
import { ServerProxy } from "../proxy"
|
import { ServerProxy } from "../proxy"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { SessionSummary } from "@/session/summary"
|
|||||||
import { Todo } from "../../session/todo"
|
import { Todo } from "../../session/todo"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
import { AppRuntime } from "../../effect/app-runtime"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { Command } from "../../command"
|
import { Command } from "../../command"
|
||||||
import { Log } from "../../util"
|
import { Log } from "../../util"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Hono } from "hono"
|
|||||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { listAdaptors } from "../../control-plane/adaptors"
|
import { listAdaptors } from "../../control-plane/adaptors"
|
||||||
import { Workspace } from "../../control-plane/workspace"
|
import { Workspace } from "../../control-plane"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { UpgradeWebSocket } from "hono/ws"
|
|||||||
import { Log } from "@/util"
|
import { Log } from "@/util"
|
||||||
import * as Fence from "./fence"
|
import * as Fence from "./fence"
|
||||||
import type { WorkspaceID } from "@/control-plane/schema"
|
import type { WorkspaceID } from "@/control-plane/schema"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane"
|
||||||
|
|
||||||
const hop = new Set([
|
const hop = new Set([
|
||||||
"connection",
|
"connection",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import z from "zod"
|
|||||||
import { Token } from "../util"
|
import { Token } from "../util"
|
||||||
import { Log } from "../util"
|
import { Log } from "../util"
|
||||||
import { SessionProcessor } from "./processor"
|
import { SessionProcessor } from "./processor"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { Config } from "@/config"
|
import { Config } from "@/config"
|
||||||
import { NotFoundError } from "@/storage"
|
import { NotFoundError } from "@/storage"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
|
|||||||
import { ProviderTransform } from "@/provider"
|
import { ProviderTransform } from "@/provider"
|
||||||
import { Config } from "@/config"
|
import { Config } from "@/config"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import type { Agent } from "@/agent/agent"
|
import type { Agent } from "@/agent"
|
||||||
import type { MessageV2 } from "./message-v2"
|
import type { MessageV2 } from "./message-v2"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect"
|
import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect"
|
||||||
import * as Stream from "effect/Stream"
|
import * as Stream from "effect/Stream"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Config } from "@/config"
|
import { Config } from "@/config"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { MessageV2 } from "./message-v2"
|
|||||||
import { Log } from "../util"
|
import { Log } from "../util"
|
||||||
import { SessionRevert } from "./revert"
|
import { SessionRevert } from "./revert"
|
||||||
import * as Session from "./session"
|
import * as Session from "./session"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent"
|
||||||
import { Provider } from "../provider"
|
import { Provider } from "../provider"
|
||||||
import { ModelID, ProviderID } from "../provider/schema"
|
import { ModelID, ProviderID } from "../provider/schema"
|
||||||
import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
|
import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
|
||||||
@@ -38,7 +38,7 @@ import { Tool } from "@/tool"
|
|||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { SessionStatus } from "./status"
|
import { SessionStatus } from "./status"
|
||||||
import { LLM } from "./llm"
|
import { LLM } from "./llm"
|
||||||
import { Shell } from "@/shell/shell"
|
import { Shell } from "@/shell"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { Truncate } from "@/tool"
|
import { Truncate } from "@/tool"
|
||||||
import { decodeDataUrl } from "@/util/data-url"
|
import { decodeDataUrl } from "@/util/data-url"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { ZodOverride } from "@/util/effect-zod"
|
import { ZodOverride } from "@/util/effect-zod"
|
||||||
import { withStatics } from "@/util/schema"
|
import { withStatics } from "@/util/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import PROMPT_KIMI from "./prompt/kimi.txt"
|
|||||||
import PROMPT_CODEX from "./prompt/codex.txt"
|
import PROMPT_CODEX from "./prompt/codex.txt"
|
||||||
import PROMPT_TRINITY from "./prompt/trinity.txt"
|
import PROMPT_TRINITY from "./prompt/trinity.txt"
|
||||||
import type { Provider } from "@/provider"
|
import type { Provider } from "@/provider"
|
||||||
import type { Agent } from "@/agent/agent"
|
import type { Agent } from "@/agent"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Skill } from "@/skill"
|
import { Skill } from "@/skill"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * as Shell from "./shell"
|
||||||
@@ -8,103 +8,101 @@ import { setTimeout as sleep } from "node:timers/promises"
|
|||||||
|
|
||||||
const SIGKILL_TIMEOUT_MS = 200
|
const SIGKILL_TIMEOUT_MS = 200
|
||||||
|
|
||||||
export namespace Shell {
|
const BLACKLIST = new Set(["fish", "nu"])
|
||||||
const BLACKLIST = new Set(["fish", "nu"])
|
const LOGIN = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"])
|
||||||
const LOGIN = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"])
|
const POSIX = new Set(["bash", "dash", "ksh", "sh", "zsh"])
|
||||||
const POSIX = new Set(["bash", "dash", "ksh", "sh", "zsh"])
|
|
||||||
|
|
||||||
export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
|
export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
|
||||||
const pid = proc.pid
|
const pid = proc.pid
|
||||||
if (!pid || opts?.exited?.()) return
|
if (!pid || opts?.exited?.()) return
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
|
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
|
||||||
stdio: "ignore",
|
stdio: "ignore",
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
})
|
|
||||||
killer.once("exit", () => resolve())
|
|
||||||
killer.once("error", () => resolve())
|
|
||||||
})
|
})
|
||||||
return
|
killer.once("exit", () => resolve())
|
||||||
}
|
killer.once("error", () => resolve())
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
process.kill(-pid, "SIGTERM")
|
process.kill(-pid, "SIGTERM")
|
||||||
await sleep(SIGKILL_TIMEOUT_MS)
|
await sleep(SIGKILL_TIMEOUT_MS)
|
||||||
if (!opts?.exited?.()) {
|
if (!opts?.exited?.()) {
|
||||||
process.kill(-pid, "SIGKILL")
|
process.kill(-pid, "SIGKILL")
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
proc.kill("SIGTERM")
|
proc.kill("SIGTERM")
|
||||||
await sleep(SIGKILL_TIMEOUT_MS)
|
await sleep(SIGKILL_TIMEOUT_MS)
|
||||||
if (!opts?.exited?.()) {
|
if (!opts?.exited?.()) {
|
||||||
proc.kill("SIGKILL")
|
proc.kill("SIGKILL")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function full(file: string) {
|
|
||||||
if (process.platform !== "win32") return file
|
|
||||||
const shell = Filesystem.windowsPath(file)
|
|
||||||
if (path.win32.dirname(shell) !== ".") {
|
|
||||||
if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell
|
|
||||||
return shell
|
|
||||||
}
|
|
||||||
return which(shell) || shell
|
|
||||||
}
|
|
||||||
|
|
||||||
function pick() {
|
|
||||||
const pwsh = which("pwsh.exe")
|
|
||||||
if (pwsh) return pwsh
|
|
||||||
const powershell = which("powershell.exe")
|
|
||||||
if (powershell) return powershell
|
|
||||||
}
|
|
||||||
|
|
||||||
function select(file: string | undefined, opts?: { acceptable?: boolean }) {
|
|
||||||
if (file && (!opts?.acceptable || !BLACKLIST.has(name(file)))) return full(file)
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
const shell = pick()
|
|
||||||
if (shell) return shell
|
|
||||||
}
|
|
||||||
return fallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gitbash() {
|
|
||||||
if (process.platform !== "win32") return
|
|
||||||
if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
|
|
||||||
const git = which("git")
|
|
||||||
if (!git) return
|
|
||||||
const file = path.join(git, "..", "..", "bin", "bash.exe")
|
|
||||||
if (Filesystem.stat(file)?.size) return file
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallback() {
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
const file = gitbash()
|
|
||||||
if (file) return file
|
|
||||||
return process.env.COMSPEC || "cmd.exe"
|
|
||||||
}
|
|
||||||
if (process.platform === "darwin") return "/bin/zsh"
|
|
||||||
const bash = which("bash")
|
|
||||||
if (bash) return bash
|
|
||||||
return "/bin/sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function name(file: string) {
|
|
||||||
if (process.platform === "win32") return path.win32.parse(Filesystem.windowsPath(file)).name.toLowerCase()
|
|
||||||
return path.basename(file).toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function login(file: string) {
|
|
||||||
return LOGIN.has(name(file))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function posix(file: string) {
|
|
||||||
return POSIX.has(name(file))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const preferred = lazy(() => select(process.env.SHELL))
|
|
||||||
|
|
||||||
export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function full(file: string) {
|
||||||
|
if (process.platform !== "win32") return file
|
||||||
|
const shell = Filesystem.windowsPath(file)
|
||||||
|
if (path.win32.dirname(shell) !== ".") {
|
||||||
|
if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell
|
||||||
|
return shell
|
||||||
|
}
|
||||||
|
return which(shell) || shell
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick() {
|
||||||
|
const pwsh = which("pwsh.exe")
|
||||||
|
if (pwsh) return pwsh
|
||||||
|
const powershell = which("powershell.exe")
|
||||||
|
if (powershell) return powershell
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(file: string | undefined, opts?: { acceptable?: boolean }) {
|
||||||
|
if (file && (!opts?.acceptable || !BLACKLIST.has(name(file)))) return full(file)
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
const shell = pick()
|
||||||
|
if (shell) return shell
|
||||||
|
}
|
||||||
|
return fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gitbash() {
|
||||||
|
if (process.platform !== "win32") return
|
||||||
|
if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
|
||||||
|
const git = which("git")
|
||||||
|
if (!git) return
|
||||||
|
const file = path.join(git, "..", "..", "bin", "bash.exe")
|
||||||
|
if (Filesystem.stat(file)?.size) return file
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallback() {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
const file = gitbash()
|
||||||
|
if (file) return file
|
||||||
|
return process.env.COMSPEC || "cmd.exe"
|
||||||
|
}
|
||||||
|
if (process.platform === "darwin") return "/bin/zsh"
|
||||||
|
const bash = which("bash")
|
||||||
|
if (bash) return bash
|
||||||
|
return "/bin/sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function name(file: string) {
|
||||||
|
if (process.platform === "win32") return path.win32.parse(Filesystem.windowsPath(file)).name.toLowerCase()
|
||||||
|
return path.basename(file).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(file: string) {
|
||||||
|
return LOGIN.has(name(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function posix(file: string) {
|
||||||
|
return POSIX.has(name(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const preferred = lazy(() => select(process.env.SHELL))
|
||||||
|
|
||||||
|
export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true }))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { pathToFileURL } from "url"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||||
import type { Agent } from "@/agent/agent"
|
import type { Agent } from "@/agent"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { ZodOverride } from "@/util/effect-zod"
|
import { ZodOverride } from "@/util/effect-zod"
|
||||||
import { withStatics } from "@/util/schema"
|
import { withStatics } from "@/util/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Language, type Node } from "web-tree-sitter"
|
|||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { Shell } from "@/shell/shell"
|
import { Shell } from "@/shell"
|
||||||
|
|
||||||
import { BashArity } from "@/permission/arity"
|
import { BashArity } from "@/permission/arity"
|
||||||
import * as Truncate from "./truncate"
|
import * as Truncate from "./truncate"
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { FileTime } from "../file/time"
|
|||||||
import { Instruction } from "../session/instruction"
|
import { Instruction } from "../session/instruction"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent"
|
||||||
import { Skill } from "../skill"
|
import { Skill } from "../skill"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { ZodOverride } from "@/util/effect-zod"
|
import { ZodOverride } from "@/util/effect-zod"
|
||||||
import { withStatics } from "@/util/schema"
|
import { withStatics } from "@/util/schema"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import z from "zod"
|
|||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import { SessionID, MessageID } from "../session/schema"
|
import { SessionID, MessageID } from "../session/schema"
|
||||||
import { MessageV2 } from "../session/message-v2"
|
import { MessageV2 } from "../session/message-v2"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent"
|
||||||
import type { SessionPrompt } from "../session/prompt"
|
import type { SessionPrompt } from "../session/prompt"
|
||||||
import { Config } from "../config"
|
import { Config } from "../config"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { MessageV2 } from "../session/message-v2"
|
|||||||
import type { Permission } from "../permission"
|
import type { Permission } from "../permission"
|
||||||
import type { SessionID, MessageID } from "../session/schema"
|
import type { SessionID, MessageID } from "../session/schema"
|
||||||
import * as Truncate from "./truncate"
|
import * as Truncate from "./truncate"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent"
|
||||||
|
|
||||||
interface Metadata {
|
interface Metadata {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NodePath } from "@effect/platform-node"
|
import { NodePath } from "@effect/platform-node"
|
||||||
import { Cause, Duration, Effect, Layer, Schedule, Context } from "effect"
|
import { Cause, Duration, Effect, Layer, Schedule, Context } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import type { Agent } from "../agent/agent"
|
import type { Agent } from "../agent"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { evaluate } from "@/permission/evaluate"
|
import { evaluate } from "@/permission/evaluate"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id"
|
||||||
import { Log } from "../util"
|
import { Log } from "../util"
|
||||||
import { ToolID } from "./schema"
|
import { ToolID } from "./schema"
|
||||||
import { TRUNCATION_DIR } from "./truncation-dir"
|
import { TRUNCATION_DIR } from "./truncation-dir"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id"
|
||||||
import { withStatics } from "@/util/schema"
|
import { withStatics } from "@/util/schema"
|
||||||
import * as DateTime from "effect/DateTime"
|
import * as DateTime from "effect/DateTime"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { ACP } from "../../src/acp/agent"
|
import { ACP } from "../../src/acp"
|
||||||
import type { Agent as ACPAgent } from "@agentclientprotocol/sdk"
|
import type { Agent as ACPAgent } from "@agentclientprotocol/sdk"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { ACP } from "../../src/acp/agent"
|
import { ACP } from "../../src/acp"
|
||||||
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
|
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
|
||||||
import type { Event, EventMessagePartUpdated, ToolStatePending, ToolStateRunning } from "@opencode-ai/sdk/v2"
|
import type { Event, EventMessagePartUpdated, ToolStatePending, ToolStateRunning } from "@opencode-ai/sdk/v2"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Effect } from "effect"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
|
|
||||||
// Helper to evaluate permission for a tool with wildcard pattern
|
// Helper to evaluate permission for a tool with wildcard pattern
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from "path"
|
|||||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Config } from "../../src/config"
|
import { Config } from "../../src/config"
|
||||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
import { Agent as AgentSvc } from "../../src/agent"
|
||||||
import { Color } from "../../src/util"
|
import { Color } from "../../src/util"
|
||||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
|
|||||||
|
|
||||||
const { Flag } = await import("../../src/flag/flag")
|
const { Flag } = await import("../../src/flag/flag")
|
||||||
const { Plugin } = await import("../../src/plugin/index")
|
const { Plugin } = await import("../../src/plugin/index")
|
||||||
const { Workspace } = await import("../../src/control-plane/workspace")
|
const { Workspace } = await import("../../src/control-plane")
|
||||||
const { Instance } = await import("../../src/project/instance")
|
const { Instance } = await import("../../src/project/instance")
|
||||||
|
|
||||||
const experimental = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
const experimental = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../src/effect/app-runtime"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Pty } from "../../src/pty"
|
import { Pty } from "../../src/pty"
|
||||||
import { Shell } from "../../src/shell/shell"
|
import { Shell } from "../../src/shell"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
Shell.preferred.reset()
|
Shell.preferred.reset()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as Stream from "effect/Stream"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { Config } from "../../src/config"
|
import { Config } from "../../src/config"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { LLM } from "../../src/session/llm"
|
import { LLM } from "../../src/session/llm"
|
||||||
import { SessionCompaction } from "../../src/session/compaction"
|
import { SessionCompaction } from "../../src/session/compaction"
|
||||||
import { Token } from "../../src/util"
|
import { Token } from "../../src/util"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ModelsDev } from "../../src/provider"
|
|||||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||||
import { Filesystem } from "../../src/util"
|
import { Filesystem } from "../../src/util"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
import type { Agent } from "../../src/agent/agent"
|
import type { Agent } from "../../src/agent"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
import { MessageV2 } from "../../src/session/message-v2"
|
||||||
import { SessionID, MessageID } from "../../src/session/schema"
|
import { SessionID, MessageID } from "../../src/session/schema"
|
||||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { NodeFileSystem } from "@effect/platform-node"
|
|||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import type { Agent } from "../../src/agent/agent"
|
import type { Agent } from "../../src/agent"
|
||||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
import { Agent as AgentSvc } from "../../src/agent"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { Config } from "../../src/config"
|
import { Config } from "../../src/config"
|
||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { FetchHttpClient } from "effect/unstable/http"
|
|||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
import { Agent as AgentSvc } from "../../src/agent"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { Command } from "../../src/command"
|
import { Command } from "../../src/command"
|
||||||
import { Config } from "../../src/config"
|
import { Config } from "../../src/config"
|
||||||
@@ -32,7 +32,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
|||||||
import { SessionStatus } from "../../src/session/status"
|
import { SessionStatus } from "../../src/session/status"
|
||||||
import { Skill } from "../../src/skill"
|
import { Skill } from "../../src/skill"
|
||||||
import { SystemPrompt } from "../../src/session/system"
|
import { SystemPrompt } from "../../src/session/system"
|
||||||
import { Shell } from "../../src/shell/shell"
|
import { Shell } from "../../src/shell"
|
||||||
import { Snapshot } from "../../src/snapshot"
|
import { Snapshot } from "../../src/snapshot"
|
||||||
import { ToolRegistry } from "../../src/tool"
|
import { ToolRegistry } from "../../src/tool"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { TestLLMServer } from "../lib/llm-server"
|
|||||||
|
|
||||||
// Same layer setup as prompt-effect.test.ts
|
// Same layer setup as prompt-effect.test.ts
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
import { Agent as AgentSvc } from "../../src/agent"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { Command } from "../../src/command"
|
import { Command } from "../../src/command"
|
||||||
import { Config } from "../../src/config"
|
import { Config } from "../../src/config"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { SystemPrompt } from "../../src/session/system"
|
import { SystemPrompt } from "../../src/session/system"
|
||||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Shell } from "../../src/shell/shell"
|
import { Shell } from "../../src/shell"
|
||||||
import { Filesystem } from "../../src/util"
|
import { Filesystem } from "../../src/util"
|
||||||
|
|
||||||
const withShell = async (shell: string | undefined, fn: () => void | Promise<void>) => {
|
const withShell = async (shell: string | undefined, fn: () => void | Promise<void>) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Instance } from "../../src/project/instance"
|
|||||||
import { SyncEvent } from "../../src/sync"
|
import { SyncEvent } from "../../src/sync"
|
||||||
import { Database } from "../../src/storage"
|
import { Database } from "../../src/storage"
|
||||||
import { EventTable } from "../../src/sync/event.sql"
|
import { EventTable } from "../../src/sync/event.sql"
|
||||||
import { Identifier } from "../../src/id/id"
|
import { Identifier } from "../../src/id"
|
||||||
import { Flag } from "../../src/flag/flag"
|
import { Flag } from "../../src/flag/flag"
|
||||||
import { initProjectors } from "../../src/server/projectors"
|
import { initProjectors } from "../../src/server/projectors"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Instance } from "../../src/project/instance"
|
|||||||
import { LSP } from "../../src/lsp"
|
import { LSP } from "../../src/lsp"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { Format } from "../../src/format"
|
import { Format } from "../../src/format"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Shell } from "../../src/shell/shell"
|
import { Shell } from "../../src/shell"
|
||||||
import { BashTool } from "../../src/tool/bash"
|
import { BashTool } from "../../src/tool/bash"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Filesystem } from "../../src/util"
|
import { Filesystem } from "../../src/util"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
import type { Permission } from "../../src/permission"
|
import type { Permission } from "../../src/permission"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { SessionID, MessageID } from "../../src/session/schema"
|
import { SessionID, MessageID } from "../../src/session/schema"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { FileTime } from "../../src/file/time"
|
|||||||
import { LSP } from "../../src/lsp"
|
import { LSP } from "../../src/lsp"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { Format } from "../../src/format"
|
import { Format } from "../../src/format"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { BusEvent } from "../../src/bus/bus-event"
|
import { BusEvent } from "../../src/bus/bus-event"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
|||||||
import { Ripgrep } from "../../src/file/ripgrep"
|
import { Ripgrep } from "../../src/file/ripgrep"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { provideInstance, provideTmpdirInstance } from "../fixture/fixture"
|
|||||||
import { SessionID, MessageID } from "../../src/session/schema"
|
import { SessionID, MessageID } from "../../src/session/schema"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Ripgrep } from "../../src/file/ripgrep"
|
import { Ripgrep } from "../../src/file/ripgrep"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Effect, Fiber, Layer } from "effect"
|
|||||||
import { QuestionTool } from "../../src/tool/question"
|
import { QuestionTool } from "../../src/tool/question"
|
||||||
import { Question } from "../../src/question"
|
import { Question } from "../../src/question"
|
||||||
import { SessionID, MessageID } from "../../src/session/schema"
|
import { SessionID, MessageID } from "../../src/session/schema"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, describe, expect } from "bun:test"
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
import { Cause, Effect, Exit, Layer } from "effect"
|
import { Cause, Effect, Exit, Layer } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { FileTime } from "../../src/file/time"
|
import { FileTime } from "../../src/file/time"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach, describe, expect } from "bun:test"
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Config } from "../../src/config"
|
import { Config } from "../../src/config"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, test, expect } from "bun:test"
|
import { describe, test, expect } from "bun:test"
|
||||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Tool } from "../../src/tool"
|
import { Tool } from "../../src/tool"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, test, expect } from "bun:test"
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { Effect, FileSystem, Layer } from "effect"
|
import { Effect, FileSystem, Layer } from "effect"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { Identifier } from "../../src/id/id"
|
import { Identifier } from "../../src/id"
|
||||||
import { Process } from "../../src/util"
|
import { Process } from "../../src/util"
|
||||||
import { Filesystem } from "../../src/util"
|
import { Filesystem } from "../../src/util"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { FetchHttpClient } from "effect/unstable/http"
|
import { FetchHttpClient } from "effect/unstable/http"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { WebFetchTool } from "../../src/tool/webfetch"
|
import { WebFetchTool } from "../../src/tool/webfetch"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Bus } from "../../src/bus"
|
|||||||
import { Format } from "../../src/format"
|
import { Format } from "../../src/format"
|
||||||
import { Truncate } from "../../src/tool"
|
import { Truncate } from "../../src/tool"
|
||||||
import { Tool } from "../../src/tool"
|
import { Tool } from "../../src/tool"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent"
|
||||||
import { SessionID, MessageID } from "../../src/session/schema"
|
import { SessionID, MessageID } from "../../src/session/schema"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
|
|||||||
Reference in New Issue
Block a user