|
|
@@ -17,6 +17,7 @@ import { Flag } from "@/flag/flag"
|
|
|
import { createRunDemo } from "./demo"
|
|
|
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo, resolveSessionInfo } from "./runtime.boot"
|
|
|
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
|
|
+import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel"
|
|
|
import { reusePendingTask } from "./runtime.shared"
|
|
|
import { trace } from "./trace"
|
|
|
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared"
|
|
|
@@ -74,418 +75,489 @@ type StreamState = {
|
|
|
// Files only attach on the first prompt turn -- after that, includeFiles
|
|
|
// flips to false so subsequent turns don't re-send attachments.
|
|
|
async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
|
|
- const start = performance.now()
|
|
|
- const log = trace()
|
|
|
- const keybindTask = resolveFooterKeybinds()
|
|
|
- const diffTask = resolveDiffStyle()
|
|
|
- const ctx = await input.boot()
|
|
|
- const modelTask = resolveModelInfo(ctx.sdk, ctx.model)
|
|
|
- const sessionTask =
|
|
|
- ctx.resume === true
|
|
|
- ? resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model)
|
|
|
- : Promise.resolve({
|
|
|
- first: true,
|
|
|
- history: [],
|
|
|
- variant: undefined,
|
|
|
+ return withRunSpan(
|
|
|
+ "RunInteractive.session",
|
|
|
+ {
|
|
|
+ "opencode.mode": input.resolveSession ? "local" : "attach",
|
|
|
+ "opencode.initial_input": !!input.initialInput,
|
|
|
+ "opencode.demo": input.demo,
|
|
|
+ },
|
|
|
+ async (span) => {
|
|
|
+ const start = performance.now()
|
|
|
+ const log = trace()
|
|
|
+ const keybindTask = resolveFooterKeybinds()
|
|
|
+ const diffTask = resolveDiffStyle()
|
|
|
+ const ctx = await input.boot()
|
|
|
+ const modelTask = resolveModelInfo(ctx.sdk, ctx.model)
|
|
|
+ const sessionTask =
|
|
|
+ ctx.resume === true
|
|
|
+ ? resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model)
|
|
|
+ : Promise.resolve({
|
|
|
+ first: true,
|
|
|
+ history: [],
|
|
|
+ variant: undefined,
|
|
|
+ })
|
|
|
+ const savedTask = resolveSavedVariant(ctx.model)
|
|
|
+ let variants: string[] = []
|
|
|
+ let limits: Record<string, number> = {}
|
|
|
+ let aborting = false
|
|
|
+ let shown = false
|
|
|
+ let demo: ReturnType<typeof createRunDemo> | undefined
|
|
|
+ const [keybinds, diffStyle, session, savedVariant] = await Promise.all([
|
|
|
+ keybindTask,
|
|
|
+ diffTask,
|
|
|
+ sessionTask,
|
|
|
+ savedTask,
|
|
|
+ ])
|
|
|
+ shown = !session.first
|
|
|
+ let activeVariant = resolveVariant(ctx.variant, session.variant, savedVariant, variants)
|
|
|
+ let sessionID = ctx.sessionID
|
|
|
+ let sessionTitle = ctx.sessionTitle
|
|
|
+ let agent = ctx.agent
|
|
|
+ let hasSession = !input.resolveSession
|
|
|
+ setRunSpanAttributes(span, {
|
|
|
+ "opencode.directory": ctx.directory,
|
|
|
+ "opencode.resume": ctx.resume === true,
|
|
|
+ "opencode.agent.name": agent,
|
|
|
+ "opencode.model.provider": ctx.model?.providerID,
|
|
|
+ "opencode.model.id": ctx.model?.modelID,
|
|
|
+ "opencode.model.variant": activeVariant,
|
|
|
+ "session.id": sessionID || undefined,
|
|
|
+ })
|
|
|
+ let resolving: Promise<void> | undefined
|
|
|
+ const ensureSession = () => {
|
|
|
+ if (!input.resolveSession) {
|
|
|
+ return Promise.resolve()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (resolving) {
|
|
|
+ return resolving
|
|
|
+ }
|
|
|
+
|
|
|
+ resolving = input.resolveSession(ctx).then((next) => {
|
|
|
+ sessionID = next.sessionID
|
|
|
+ sessionTitle = next.sessionTitle
|
|
|
+ agent = next.agent
|
|
|
+ hasSession = true
|
|
|
+ setRunSpanAttributes(span, {
|
|
|
+ "opencode.agent.name": agent,
|
|
|
+ "session.id": sessionID,
|
|
|
+ })
|
|
|
})
|
|
|
- const savedTask = resolveSavedVariant(ctx.model)
|
|
|
- let variants: string[] = []
|
|
|
- let limits: Record<string, number> = {}
|
|
|
- let aborting = false
|
|
|
- let shown = false
|
|
|
- let demo: ReturnType<typeof createRunDemo> | undefined
|
|
|
- const [keybinds, diffStyle, session, savedVariant] = await Promise.all([
|
|
|
- keybindTask,
|
|
|
- diffTask,
|
|
|
- sessionTask,
|
|
|
- savedTask,
|
|
|
- ])
|
|
|
- shown = !session.first
|
|
|
- let activeVariant = resolveVariant(ctx.variant, session.variant, savedVariant, variants)
|
|
|
- let sessionID = ctx.sessionID
|
|
|
- let sessionTitle = ctx.sessionTitle
|
|
|
- let agent = ctx.agent
|
|
|
- let hasSession = !input.resolveSession
|
|
|
- let resolving: Promise<void> | undefined
|
|
|
- const ensureSession = () => {
|
|
|
- if (!input.resolveSession) {
|
|
|
- return Promise.resolve()
|
|
|
- }
|
|
|
-
|
|
|
- if (resolving) {
|
|
|
- return resolving
|
|
|
- }
|
|
|
-
|
|
|
- resolving = input.resolveSession(ctx).then((next) => {
|
|
|
- sessionID = next.sessionID
|
|
|
- sessionTitle = next.sessionTitle
|
|
|
- agent = next.agent
|
|
|
- hasSession = true
|
|
|
- })
|
|
|
- return resolving
|
|
|
- }
|
|
|
- let selectSubagent: ((sessionID: string | undefined) => void) | undefined
|
|
|
-
|
|
|
- const shell = await createRuntimeLifecycle({
|
|
|
- directory: ctx.directory,
|
|
|
- findFiles: (query) =>
|
|
|
- ctx.sdk.find
|
|
|
- .files({ query, directory: ctx.directory })
|
|
|
- .then((x) => x.data ?? [])
|
|
|
- .catch(() => []),
|
|
|
- agents: [],
|
|
|
- resources: [],
|
|
|
- sessionID,
|
|
|
- sessionTitle,
|
|
|
- first: session.first,
|
|
|
- history: session.history,
|
|
|
- agent,
|
|
|
- model: ctx.model,
|
|
|
- variant: activeVariant,
|
|
|
- keybinds,
|
|
|
- diffStyle,
|
|
|
- onPermissionReply: async (next) => {
|
|
|
- if (demo?.permission(next)) {
|
|
|
- return
|
|
|
+ return resolving
|
|
|
}
|
|
|
+ let selectSubagent: ((sessionID: string | undefined) => void) | undefined
|
|
|
+
|
|
|
+ const shell = await createRuntimeLifecycle({
|
|
|
+ directory: ctx.directory,
|
|
|
+ findFiles: (query) =>
|
|
|
+ ctx.sdk.find
|
|
|
+ .files({ query, directory: ctx.directory })
|
|
|
+ .then((x) => x.data ?? [])
|
|
|
+ .catch(() => []),
|
|
|
+ agents: [],
|
|
|
+ resources: [],
|
|
|
+ sessionID,
|
|
|
+ sessionTitle,
|
|
|
+ getSessionID: () => sessionID,
|
|
|
+ first: session.first,
|
|
|
+ history: session.history,
|
|
|
+ agent,
|
|
|
+ model: ctx.model,
|
|
|
+ variant: activeVariant,
|
|
|
+ keybinds,
|
|
|
+ diffStyle,
|
|
|
+ onPermissionReply: async (next) => {
|
|
|
+ if (demo?.permission(next)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- log?.write("send.permission.reply", next)
|
|
|
- await ctx.sdk.permission.reply(next)
|
|
|
- },
|
|
|
- onQuestionReply: async (next) => {
|
|
|
- if (demo?.questionReply(next)) {
|
|
|
- return
|
|
|
- }
|
|
|
+ log?.write("send.permission.reply", next)
|
|
|
+ await ctx.sdk.permission.reply(next)
|
|
|
+ },
|
|
|
+ onQuestionReply: async (next) => {
|
|
|
+ if (demo?.questionReply(next)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- await ctx.sdk.question.reply(next)
|
|
|
- },
|
|
|
- onQuestionReject: async (next) => {
|
|
|
- if (demo?.questionReject(next)) {
|
|
|
- return
|
|
|
- }
|
|
|
+ await ctx.sdk.question.reply(next)
|
|
|
+ },
|
|
|
+ onQuestionReject: async (next) => {
|
|
|
+ if (demo?.questionReject(next)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- await ctx.sdk.question.reject(next)
|
|
|
- },
|
|
|
- onCycleVariant: () => {
|
|
|
- if (!ctx.model || variants.length === 0) {
|
|
|
- return {
|
|
|
- status: "no variants available",
|
|
|
+ await ctx.sdk.question.reject(next)
|
|
|
+ },
|
|
|
+ onCycleVariant: () => {
|
|
|
+ if (!ctx.model || variants.length === 0) {
|
|
|
+ return {
|
|
|
+ status: "no variants available",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ activeVariant = cycleVariant(activeVariant, variants)
|
|
|
+ saveVariant(ctx.model, activeVariant)
|
|
|
+ setRunSpanAttributes(span, {
|
|
|
+ "opencode.model.variant": activeVariant,
|
|
|
+ })
|
|
|
+ return {
|
|
|
+ status: activeVariant ? `variant ${activeVariant}` : "variant default",
|
|
|
+ modelLabel: formatModelLabel(ctx.model, activeVariant),
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onInterrupt: () => {
|
|
|
+ if (!hasSession) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (aborting) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ aborting = true
|
|
|
+ void ctx.sdk.session
|
|
|
+ .abort({
|
|
|
+ sessionID,
|
|
|
+ })
|
|
|
+ .catch(() => {})
|
|
|
+ .finally(() => {
|
|
|
+ aborting = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onSubagentSelect: (sessionID) => {
|
|
|
+ selectSubagent?.(sessionID)
|
|
|
+ log?.write("subagent.select", {
|
|
|
+ sessionID,
|
|
|
+ })
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const footer = shell.footer
|
|
|
+
|
|
|
+ let catalogTask: Promise<void> | undefined
|
|
|
+ const loadCatalog = () => {
|
|
|
+ if (catalogTask) {
|
|
|
+ return catalogTask
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- activeVariant = cycleVariant(activeVariant, variants)
|
|
|
- saveVariant(ctx.model, activeVariant)
|
|
|
- return {
|
|
|
- status: activeVariant ? `variant ${activeVariant}` : "variant default",
|
|
|
- modelLabel: formatModelLabel(ctx.model, activeVariant),
|
|
|
- }
|
|
|
- },
|
|
|
- onInterrupt: () => {
|
|
|
- if (!hasSession) {
|
|
|
- return
|
|
|
+ catalogTask = Promise.all([
|
|
|
+ ctx.sdk.app
|
|
|
+ .agents({ directory: ctx.directory })
|
|
|
+ .then((x) => x.data ?? [])
|
|
|
+ .catch(() => []),
|
|
|
+ ctx.sdk.experimental.resource
|
|
|
+ .list({ directory: ctx.directory })
|
|
|
+ .then((x) => Object.values(x.data ?? {}))
|
|
|
+ .catch(() => []),
|
|
|
+ ])
|
|
|
+ .then(([agents, resources]) => {
|
|
|
+ if (footer.isClosed) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ footer.event({
|
|
|
+ type: "catalog",
|
|
|
+ agents,
|
|
|
+ resources,
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .catch(() => {})
|
|
|
+
|
|
|
+ return catalogTask
|
|
|
}
|
|
|
|
|
|
- if (aborting) {
|
|
|
- return
|
|
|
+ void footer
|
|
|
+ .idle()
|
|
|
+ .then(() => {
|
|
|
+ if (footer.isClosed) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ void loadCatalog()
|
|
|
+ })
|
|
|
+ .catch(() => {})
|
|
|
+
|
|
|
+ if (Flag.OPENCODE_SHOW_TTFD) {
|
|
|
+ footer.append({
|
|
|
+ kind: "system",
|
|
|
+ text: `startup ${Math.max(0, Math.round(performance.now() - start))}ms`,
|
|
|
+ phase: "final",
|
|
|
+ source: "system",
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- aborting = true
|
|
|
- void ctx.sdk.session
|
|
|
- .abort({
|
|
|
+ if (input.demo) {
|
|
|
+ await ensureSession()
|
|
|
+ demo = createRunDemo({
|
|
|
+ mode: input.demo,
|
|
|
+ text: input.demoText,
|
|
|
+ footer,
|
|
|
sessionID,
|
|
|
+ thinking: input.thinking,
|
|
|
+ limits: () => limits,
|
|
|
})
|
|
|
- .catch(() => {})
|
|
|
- .finally(() => {
|
|
|
- aborting = false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (input.afterPaint) {
|
|
|
+ void Promise.resolve(input.afterPaint(ctx)).catch(() => {})
|
|
|
+ }
|
|
|
+
|
|
|
+ void modelTask.then((info) => {
|
|
|
+ variants = info.variants
|
|
|
+ limits = info.limits
|
|
|
+
|
|
|
+ const next = resolveVariant(ctx.variant, session.variant, savedVariant, variants)
|
|
|
+ if (next === activeVariant) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ activeVariant = next
|
|
|
+ setRunSpanAttributes(span, {
|
|
|
+ "opencode.model.variant": activeVariant,
|
|
|
})
|
|
|
- },
|
|
|
- onSubagentSelect: (sessionID) => {
|
|
|
- selectSubagent?.(sessionID)
|
|
|
- log?.write("subagent.select", {
|
|
|
- sessionID,
|
|
|
- })
|
|
|
- },
|
|
|
- })
|
|
|
- const footer = shell.footer
|
|
|
-
|
|
|
- let catalogTask: Promise<void> | undefined
|
|
|
- const loadCatalog = () => {
|
|
|
- if (catalogTask) {
|
|
|
- return catalogTask
|
|
|
- }
|
|
|
-
|
|
|
- catalogTask = Promise.all([
|
|
|
- ctx.sdk.app
|
|
|
- .agents({ directory: ctx.directory })
|
|
|
- .then((x) => x.data ?? [])
|
|
|
- .catch(() => []),
|
|
|
- ctx.sdk.experimental.resource
|
|
|
- .list({ directory: ctx.directory })
|
|
|
- .then((x) => Object.values(x.data ?? {}))
|
|
|
- .catch(() => []),
|
|
|
- ])
|
|
|
- .then(([agents, resources]) => {
|
|
|
- if (footer.isClosed) {
|
|
|
+ if (!ctx.model || footer.isClosed) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
footer.event({
|
|
|
- type: "catalog",
|
|
|
- agents,
|
|
|
- resources,
|
|
|
+ type: "model",
|
|
|
+ model: formatModelLabel(ctx.model, activeVariant),
|
|
|
})
|
|
|
})
|
|
|
- .catch(() => {})
|
|
|
|
|
|
- return catalogTask
|
|
|
- }
|
|
|
+ const streamTask = import("./stream.transport")
|
|
|
+ let stream: StreamState | undefined
|
|
|
+ const loading: { current?: Promise<StreamState> } = {}
|
|
|
+ const ensureStream = () => {
|
|
|
+ if (stream) {
|
|
|
+ return Promise.resolve(stream)
|
|
|
+ }
|
|
|
|
|
|
- void footer
|
|
|
- .idle()
|
|
|
- .then(() => {
|
|
|
- if (footer.isClosed) {
|
|
|
- return
|
|
|
- }
|
|
|
+ return reusePendingTask(loading, async () => {
|
|
|
+ await ensureSession()
|
|
|
+ if (footer.isClosed) {
|
|
|
+ throw new Error("runtime closed")
|
|
|
+ }
|
|
|
|
|
|
- void loadCatalog()
|
|
|
- })
|
|
|
- .catch(() => {})
|
|
|
-
|
|
|
- if (Flag.OPENCODE_SHOW_TTFD) {
|
|
|
- footer.append({
|
|
|
- kind: "system",
|
|
|
- text: `startup ${Math.max(0, Math.round(performance.now() - start))}ms`,
|
|
|
- phase: "final",
|
|
|
- source: "system",
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- if (input.demo) {
|
|
|
- await ensureSession()
|
|
|
- demo = createRunDemo({
|
|
|
- mode: input.demo,
|
|
|
- text: input.demoText,
|
|
|
- footer,
|
|
|
- sessionID,
|
|
|
- thinking: input.thinking,
|
|
|
- limits: () => limits,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- if (input.afterPaint) {
|
|
|
- void Promise.resolve(input.afterPaint(ctx)).catch(() => {})
|
|
|
- }
|
|
|
-
|
|
|
- void modelTask.then((info) => {
|
|
|
- variants = info.variants
|
|
|
- limits = info.limits
|
|
|
-
|
|
|
- const next = resolveVariant(ctx.variant, session.variant, savedVariant, variants)
|
|
|
- if (next === activeVariant) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- activeVariant = next
|
|
|
- if (!ctx.model || footer.isClosed) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- footer.event({
|
|
|
- type: "model",
|
|
|
- model: formatModelLabel(ctx.model, activeVariant),
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- const streamTask = import("./stream.transport")
|
|
|
- let stream: StreamState | undefined
|
|
|
- const loading: { current?: Promise<StreamState> } = {}
|
|
|
- const ensureStream = () => {
|
|
|
- if (stream) {
|
|
|
- return Promise.resolve(stream)
|
|
|
- }
|
|
|
-
|
|
|
- return reusePendingTask(loading, async () => {
|
|
|
- await ensureSession()
|
|
|
- if (footer.isClosed) {
|
|
|
- throw new Error("runtime closed")
|
|
|
- }
|
|
|
+ const mod = await streamTask
|
|
|
+ if (footer.isClosed) {
|
|
|
+ throw new Error("runtime closed")
|
|
|
+ }
|
|
|
|
|
|
- const mod = await streamTask
|
|
|
- if (footer.isClosed) {
|
|
|
- throw new Error("runtime closed")
|
|
|
- }
|
|
|
+ const handle = await mod.createSessionTransport({
|
|
|
+ sdk: ctx.sdk,
|
|
|
+ sessionID,
|
|
|
+ thinking: input.thinking,
|
|
|
+ limits: () => limits,
|
|
|
+ footer,
|
|
|
+ trace: log,
|
|
|
+ })
|
|
|
+ if (footer.isClosed) {
|
|
|
+ await handle.close()
|
|
|
+ throw new Error("runtime closed")
|
|
|
+ }
|
|
|
|
|
|
- const handle = await mod.createSessionTransport({
|
|
|
- sdk: ctx.sdk,
|
|
|
- sessionID,
|
|
|
- thinking: input.thinking,
|
|
|
- limits: () => limits,
|
|
|
- footer,
|
|
|
- trace: log,
|
|
|
- })
|
|
|
- if (footer.isClosed) {
|
|
|
- await handle.close()
|
|
|
- throw new Error("runtime closed")
|
|
|
+ selectSubagent = handle.selectSubagent
|
|
|
+ const next = { mod, handle }
|
|
|
+ stream = next
|
|
|
+ return next
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- selectSubagent = handle.selectSubagent
|
|
|
- const next = { mod, handle }
|
|
|
- stream = next
|
|
|
- return next
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const runQueue = async () => {
|
|
|
- let includeFiles = true
|
|
|
- if (demo) {
|
|
|
- await demo.start()
|
|
|
- }
|
|
|
-
|
|
|
- const mod = await import("./runtime.queue")
|
|
|
- await mod.runPromptQueue({
|
|
|
- footer,
|
|
|
- initialInput: input.initialInput,
|
|
|
- trace: log,
|
|
|
- onPrompt: () => {
|
|
|
- shown = true
|
|
|
- },
|
|
|
- run: async (prompt, signal) => {
|
|
|
- if (demo && (await demo.prompt(prompt, signal))) {
|
|
|
- return
|
|
|
+ const runQueue = async () => {
|
|
|
+ let includeFiles = true
|
|
|
+ if (demo) {
|
|
|
+ await demo.start()
|
|
|
}
|
|
|
|
|
|
- try {
|
|
|
- const next = await ensureStream()
|
|
|
- await next.handle.runPromptTurn({
|
|
|
- agent,
|
|
|
- model: ctx.model,
|
|
|
- variant: activeVariant,
|
|
|
- prompt,
|
|
|
- files: input.files,
|
|
|
- includeFiles,
|
|
|
- signal,
|
|
|
- })
|
|
|
- includeFiles = false
|
|
|
- } catch (error) {
|
|
|
- if (signal.aborted || footer.isClosed) {
|
|
|
- return
|
|
|
- }
|
|
|
+ const mod = await import("./runtime.queue")
|
|
|
+ await mod.runPromptQueue({
|
|
|
+ footer,
|
|
|
+ initialInput: input.initialInput,
|
|
|
+ trace: log,
|
|
|
+ onPrompt: () => {
|
|
|
+ shown = true
|
|
|
+ },
|
|
|
+ run: async (prompt, signal) => {
|
|
|
+ if (demo && (await demo.prompt(prompt, signal))) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ return withRunSpan(
|
|
|
+ "RunInteractive.turn",
|
|
|
+ {
|
|
|
+ "opencode.agent.name": agent,
|
|
|
+ "opencode.model.provider": ctx.model?.providerID,
|
|
|
+ "opencode.model.id": ctx.model?.modelID,
|
|
|
+ "opencode.model.variant": activeVariant,
|
|
|
+ "opencode.prompt.chars": prompt.text.length,
|
|
|
+ "opencode.prompt.parts": prompt.parts.length,
|
|
|
+ "opencode.prompt.include_files": includeFiles,
|
|
|
+ "opencode.prompt.file_parts": includeFiles ? input.files.length : 0,
|
|
|
+ "session.id": sessionID || undefined,
|
|
|
+ },
|
|
|
+ async (span) => {
|
|
|
+ try {
|
|
|
+ const next = await ensureStream()
|
|
|
+ setRunSpanAttributes(span, {
|
|
|
+ "opencode.agent.name": agent,
|
|
|
+ "opencode.model.variant": activeVariant,
|
|
|
+ "session.id": sessionID || undefined,
|
|
|
+ })
|
|
|
+ await next.handle.runPromptTurn({
|
|
|
+ agent,
|
|
|
+ model: ctx.model,
|
|
|
+ variant: activeVariant,
|
|
|
+ prompt,
|
|
|
+ files: input.files,
|
|
|
+ includeFiles,
|
|
|
+ signal,
|
|
|
+ })
|
|
|
+ includeFiles = false
|
|
|
+ } catch (error) {
|
|
|
+ if (signal.aborted || footer.isClosed) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ recordRunSpanError(span, error)
|
|
|
+ const text =
|
|
|
+ stream?.mod.formatUnknownError(error) ?? (error instanceof Error ? error.message : String(error))
|
|
|
+ footer.append({ kind: "error", text, phase: "start", source: "system" })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ )
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- const text =
|
|
|
- stream?.mod.formatUnknownError(error) ?? (error instanceof Error ? error.message : String(error))
|
|
|
- footer.append({ kind: "error", text, phase: "start", source: "system" })
|
|
|
+ try {
|
|
|
+ const eager = ctx.resume === true || !input.resolveSession || !!input.demo
|
|
|
+ if (eager) {
|
|
|
+ await ensureStream()
|
|
|
}
|
|
|
- },
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const eager = ctx.resume === true || !input.resolveSession || !!input.demo
|
|
|
- if (eager) {
|
|
|
- await ensureStream()
|
|
|
- }
|
|
|
-
|
|
|
- if (!eager && input.resolveSession) {
|
|
|
- queueMicrotask(() => {
|
|
|
- if (footer.isClosed) {
|
|
|
- return
|
|
|
+
|
|
|
+ if (!eager && input.resolveSession) {
|
|
|
+ queueMicrotask(() => {
|
|
|
+ if (footer.isClosed) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ void ensureStream().catch(() => {})
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- void ensureStream().catch(() => {})
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await runQueue()
|
|
|
- } finally {
|
|
|
- await stream?.handle.close()
|
|
|
- }
|
|
|
- } finally {
|
|
|
- const title =
|
|
|
- shown && hasSession
|
|
|
- ? await ctx.sdk.session
|
|
|
- .get({
|
|
|
- sessionID,
|
|
|
- })
|
|
|
- .then((x) => x.data?.title)
|
|
|
- .catch(() => undefined)
|
|
|
- : undefined
|
|
|
-
|
|
|
- await shell.close({
|
|
|
- showExit: shown && hasSession,
|
|
|
- sessionTitle: title,
|
|
|
- sessionID,
|
|
|
- })
|
|
|
- }
|
|
|
+ try {
|
|
|
+ await runQueue()
|
|
|
+ } finally {
|
|
|
+ await stream?.handle.close()
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ const title =
|
|
|
+ shown && hasSession
|
|
|
+ ? await ctx.sdk.session
|
|
|
+ .get({
|
|
|
+ sessionID,
|
|
|
+ })
|
|
|
+ .then((x) => x.data?.title)
|
|
|
+ .catch(() => undefined)
|
|
|
+ : undefined
|
|
|
+
|
|
|
+ await shell.close({
|
|
|
+ showExit: shown && hasSession,
|
|
|
+ sessionTitle: title,
|
|
|
+ sessionID,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
// Local in-process mode. Creates an SDK client backed by a direct fetch to
|
|
|
// the in-process server, so no external HTTP server is needed.
|
|
|
export async function runInteractiveLocalMode(input: RunLocalInput): Promise<void> {
|
|
|
- const sdk = createOpencodeClient({
|
|
|
- baseUrl: "http://opencode.internal",
|
|
|
- fetch: input.fetch,
|
|
|
- directory: input.directory,
|
|
|
- })
|
|
|
- let pending: Promise<{ sessionID: string; sessionTitle?: string; agent?: string | undefined }> | undefined
|
|
|
-
|
|
|
- return runInteractiveRuntime({
|
|
|
- files: input.files,
|
|
|
- initialInput: input.initialInput,
|
|
|
- thinking: input.thinking,
|
|
|
- demo: input.demo,
|
|
|
- demoText: input.demoText,
|
|
|
- resolveSession: () => {
|
|
|
- if (pending) {
|
|
|
- return pending
|
|
|
- }
|
|
|
+ return withRunSpan(
|
|
|
+ "RunInteractive.localMode",
|
|
|
+ {
|
|
|
+ "opencode.directory": input.directory,
|
|
|
+ "opencode.initial_input": !!input.initialInput,
|
|
|
+ "opencode.demo": input.demo,
|
|
|
+ },
|
|
|
+ async () => {
|
|
|
+ const sdk = createOpencodeClient({
|
|
|
+ baseUrl: "http://opencode.internal",
|
|
|
+ fetch: input.fetch,
|
|
|
+ directory: input.directory,
|
|
|
+ })
|
|
|
+ let pending: Promise<{ sessionID: string; sessionTitle?: string; agent?: string | undefined }> | undefined
|
|
|
|
|
|
- pending = Promise.all([input.resolveAgent(), input.session(sdk)]).then(([agent, session]) => {
|
|
|
- if (!session?.id) {
|
|
|
- throw new Error("Session not found")
|
|
|
- }
|
|
|
+ return runInteractiveRuntime({
|
|
|
+ files: input.files,
|
|
|
+ initialInput: input.initialInput,
|
|
|
+ thinking: input.thinking,
|
|
|
+ demo: input.demo,
|
|
|
+ demoText: input.demoText,
|
|
|
+ resolveSession: () => {
|
|
|
+ if (pending) {
|
|
|
+ return pending
|
|
|
+ }
|
|
|
|
|
|
- void input.share(sdk, session.id).catch(() => {})
|
|
|
- return {
|
|
|
- sessionID: session.id,
|
|
|
- sessionTitle: session.title,
|
|
|
- agent,
|
|
|
- }
|
|
|
+ pending = Promise.all([input.resolveAgent(), input.session(sdk)]).then(([agent, session]) => {
|
|
|
+ if (!session?.id) {
|
|
|
+ throw new Error("Session not found")
|
|
|
+ }
|
|
|
+
|
|
|
+ void input.share(sdk, session.id).catch(() => {})
|
|
|
+ return {
|
|
|
+ sessionID: session.id,
|
|
|
+ sessionTitle: session.title,
|
|
|
+ agent,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return pending
|
|
|
+ },
|
|
|
+ boot: async () => {
|
|
|
+ return {
|
|
|
+ sdk,
|
|
|
+ directory: input.directory,
|
|
|
+ sessionID: "",
|
|
|
+ sessionTitle: undefined,
|
|
|
+ resume: false,
|
|
|
+ agent: input.agent,
|
|
|
+ model: input.model,
|
|
|
+ variant: input.variant,
|
|
|
+ }
|
|
|
+ },
|
|
|
})
|
|
|
- return pending
|
|
|
- },
|
|
|
- boot: async () => {
|
|
|
- return {
|
|
|
- sdk,
|
|
|
- directory: input.directory,
|
|
|
- sessionID: "",
|
|
|
- sessionTitle: undefined,
|
|
|
- resume: false,
|
|
|
- agent: input.agent,
|
|
|
- model: input.model,
|
|
|
- variant: input.variant,
|
|
|
- }
|
|
|
},
|
|
|
- })
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
// Attach mode. Uses the caller-provided SDK client directly.
|
|
|
export async function runInteractiveMode(input: RunInput): Promise<void> {
|
|
|
- return runInteractiveRuntime({
|
|
|
- files: input.files,
|
|
|
- initialInput: input.initialInput,
|
|
|
- thinking: input.thinking,
|
|
|
- demo: input.demo,
|
|
|
- demoText: input.demoText,
|
|
|
- boot: async () => ({
|
|
|
- sdk: input.sdk,
|
|
|
- directory: input.directory,
|
|
|
- sessionID: input.sessionID,
|
|
|
- sessionTitle: input.sessionTitle,
|
|
|
- resume: input.resume,
|
|
|
- agent: input.agent,
|
|
|
- model: input.model,
|
|
|
- variant: input.variant,
|
|
|
- }),
|
|
|
- })
|
|
|
+ return withRunSpan(
|
|
|
+ "RunInteractive.attachMode",
|
|
|
+ {
|
|
|
+ "opencode.directory": input.directory,
|
|
|
+ "opencode.initial_input": !!input.initialInput,
|
|
|
+ "session.id": input.sessionID,
|
|
|
+ },
|
|
|
+ async () =>
|
|
|
+ runInteractiveRuntime({
|
|
|
+ files: input.files,
|
|
|
+ initialInput: input.initialInput,
|
|
|
+ thinking: input.thinking,
|
|
|
+ demo: input.demo,
|
|
|
+ demoText: input.demoText,
|
|
|
+ boot: async () => ({
|
|
|
+ sdk: input.sdk,
|
|
|
+ directory: input.directory,
|
|
|
+ sessionID: input.sessionID,
|
|
|
+ sessionTitle: input.sessionTitle,
|
|
|
+ resume: input.resume,
|
|
|
+ agent: input.agent,
|
|
|
+ model: input.model,
|
|
|
+ variant: input.variant,
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ )
|
|
|
}
|