mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
sync
This commit is contained in:
parent
d6f2864622
commit
7718aec891
8 changed files with 31 additions and 246 deletions
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
description: Git commit and push
|
||||
subtask: true
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
"plugin": ["opencode-openai-codex-auth"],
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {
|
||||
"baseURL": "http://localhost:8080"
|
||||
}
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import { Global } from "../global"
|
|||
import { ProjectRoute } from "./project"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { SessionLock } from "../session/lock"
|
||||
import { SessionPrompt } from "../session/prompt"
|
||||
import { SessionCompaction } from "../session/compaction"
|
||||
import { SessionRevert } from "../session/revert"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { SessionPrompt } from "./prompt"
|
|||
import { Flag } from "../flag/flag"
|
||||
import { Token } from "../util/token"
|
||||
import { Log } from "../util/log"
|
||||
import { SessionLock } from "./lock"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { SessionRetry } from "./retry"
|
||||
import { Config } from "@/config/config"
|
||||
|
|
@ -88,7 +87,6 @@ export namespace SessionCompaction {
|
|||
}
|
||||
|
||||
export async function run(input: { sessionID: string; providerID: string; modelID: string; signal?: AbortSignal }) {
|
||||
if (!input.signal) SessionLock.assertUnlocked(input.sessionID)
|
||||
await using lock = input.signal === undefined ? SessionLock.acquire({ sessionID: input.sessionID }) : undefined
|
||||
const signal = input.signal ?? lock!.signal
|
||||
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
import z from "zod"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Log } from "../util/log"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace SessionLock {
|
||||
const log = Log.create({ service: "session.lock" })
|
||||
|
||||
export const LockedError = NamedError.create(
|
||||
"SessionLockedError",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
type LockState = {
|
||||
controller: AbortController
|
||||
created: number
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
() => {
|
||||
const locks = new Map<string, LockState>()
|
||||
return {
|
||||
locks,
|
||||
}
|
||||
},
|
||||
async (current) => {
|
||||
for (const [sessionID, lock] of current.locks) {
|
||||
log.info("force abort", { sessionID })
|
||||
lock.controller.abort()
|
||||
}
|
||||
current.locks.clear()
|
||||
},
|
||||
)
|
||||
|
||||
function get(sessionID: string) {
|
||||
return state().locks.get(sessionID)
|
||||
}
|
||||
|
||||
function unset(input: { sessionID: string; controller: AbortController }) {
|
||||
const lock = get(input.sessionID)
|
||||
if (!lock) return false
|
||||
if (lock.controller !== input.controller) return false
|
||||
state().locks.delete(input.sessionID)
|
||||
return true
|
||||
}
|
||||
|
||||
export function acquire(input: { sessionID: string }) {
|
||||
const lock = get(input.sessionID)
|
||||
if (lock) {
|
||||
throw new LockedError({
|
||||
sessionID: input.sessionID,
|
||||
message: `Session ${input.sessionID} is locked`,
|
||||
})
|
||||
}
|
||||
const controller = new AbortController()
|
||||
state().locks.set(input.sessionID, {
|
||||
controller,
|
||||
created: Date.now(),
|
||||
})
|
||||
log.info("locked", { sessionID: input.sessionID })
|
||||
return {
|
||||
signal: controller.signal,
|
||||
abort() {
|
||||
controller.abort()
|
||||
unset({ sessionID: input.sessionID, controller })
|
||||
},
|
||||
async [Symbol.dispose]() {
|
||||
const removed = unset({ sessionID: input.sessionID, controller })
|
||||
if (removed) {
|
||||
log.info("unlocked", { sessionID: input.sessionID })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function abort(sessionID: string) {
|
||||
const lock = get(sessionID)
|
||||
if (!lock) return false
|
||||
log.info("abort", { sessionID })
|
||||
lock.controller.abort()
|
||||
state().locks.delete(sessionID)
|
||||
return true
|
||||
}
|
||||
|
||||
export function isLocked(sessionID: string) {
|
||||
return get(sessionID) !== undefined
|
||||
}
|
||||
|
||||
export function assertUnlocked(sessionID: string) {
|
||||
const lock = get(sessionID)
|
||||
if (!lock) return
|
||||
throw new LockedError({ sessionID, message: `Session ${sessionID} is locked` })
|
||||
}
|
||||
}
|
||||
|
|
@ -49,14 +49,12 @@ import { $, fileURLToPath } from "bun"
|
|||
import { ConfigMarkdown } from "../config/markdown"
|
||||
import { SessionSummary } from "./summary"
|
||||
import { NamedError } from "@/util/error"
|
||||
import { SessionLock } from "./lock"
|
||||
import { fn } from "@/util/fn"
|
||||
import { SessionRetry } from "./retry"
|
||||
|
||||
export namespace SessionPrompt {
|
||||
const log = Log.create({ service: "session.prompt" })
|
||||
export const OUTPUT_TOKEN_MAX = 32_000
|
||||
const MAX_RETRIES = 10
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
|
||||
export const Status = z
|
||||
|
|
@ -132,6 +130,19 @@ export namespace SessionPrompt {
|
|||
return state().status
|
||||
}
|
||||
|
||||
export function getStatus(sessionID: string) {
|
||||
return (
|
||||
state().status[sessionID] ?? {
|
||||
type: "idle",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function assertNotBusy(sessionID: string) {
|
||||
const status = getStatus(sessionID)
|
||||
if (status?.type !== "idle") throw new Session.BusyError(sessionID)
|
||||
}
|
||||
|
||||
export const setStatus = fn(z.object({ sessionID: Identifier.schema("session"), status: Status }), (input) => {
|
||||
Bus.publish(Event.Status, { sessionID: input.sessionID, status: input.status })
|
||||
if (input.status.type === "idle") {
|
||||
|
|
@ -1387,7 +1398,6 @@ export namespace SessionPrompt {
|
|||
})
|
||||
export type ShellInput = z.infer<typeof ShellInput>
|
||||
export async function shell(input: ShellInput) {
|
||||
using abort = SessionLock.acquire({ sessionID: input.sessionID })
|
||||
const session = await Session.get(input.sessionID)
|
||||
if (session.revert) {
|
||||
SessionRevert.cleanup(session)
|
||||
|
|
@ -1502,7 +1512,6 @@ export namespace SessionPrompt {
|
|||
|
||||
const proc = spawn(shell, args, {
|
||||
cwd: Instance.directory,
|
||||
signal: abort.signal,
|
||||
detached: true,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
|
|
@ -1511,11 +1520,6 @@ export namespace SessionPrompt {
|
|||
},
|
||||
})
|
||||
|
||||
abort.signal.addEventListener("abort", () => {
|
||||
if (!proc.pid) return
|
||||
process.kill(-proc.pid)
|
||||
})
|
||||
|
||||
let output = ""
|
||||
|
||||
proc.stdout?.on("data", (chunk) => {
|
||||
|
|
@ -1646,132 +1650,22 @@ export namespace SessionPrompt {
|
|||
})()
|
||||
|
||||
const agent = await Agent.get(agentName)
|
||||
let result: MessageV2.WithParts
|
||||
|
||||
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
|
||||
using abort = SessionLock.acquire({ sessionID: input.sessionID })
|
||||
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
role: "user",
|
||||
agent: agentName,
|
||||
model: {
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
},
|
||||
}
|
||||
await Session.updateMessage(userMsg)
|
||||
const userPart: MessageV2.Part = {
|
||||
type: "text",
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
text: "The following tool was executed by the user",
|
||||
synthetic: true,
|
||||
}
|
||||
await Session.updatePart(userPart)
|
||||
|
||||
const assistantMsg: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
parentID: userMsg.id,
|
||||
mode: agentName,
|
||||
cost: 0,
|
||||
path: {
|
||||
cwd: Instance.directory,
|
||||
root: Instance.worktree,
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
role: "assistant",
|
||||
tokens: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
}
|
||||
await Session.updateMessage(assistantMsg)
|
||||
|
||||
const args = {
|
||||
description: "Consulting " + agent.name,
|
||||
subagent_type: agent.name,
|
||||
prompt: template,
|
||||
}
|
||||
const toolPart: MessageV2.ToolPart = {
|
||||
type: "tool",
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: assistantMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
tool: "task",
|
||||
callID: ulid(),
|
||||
state: {
|
||||
status: "running",
|
||||
time: {
|
||||
start: Date.now(),
|
||||
},
|
||||
input: {
|
||||
description: args.description,
|
||||
subagent_type: args.subagent_type,
|
||||
// truncate prompt to preserve context
|
||||
prompt: args.prompt.length > 100 ? args.prompt.substring(0, 97) + "..." : args.prompt,
|
||||
},
|
||||
},
|
||||
}
|
||||
await Session.updatePart(toolPart)
|
||||
|
||||
const taskResult = await TaskTool.init().then((t) =>
|
||||
t.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: abort.signal,
|
||||
agent: agent.name,
|
||||
messageID: assistantMsg.id,
|
||||
extra: {},
|
||||
metadata: async (metadata) => {
|
||||
if (toolPart.state.status === "running") {
|
||||
toolPart.state.metadata = metadata.metadata
|
||||
toolPart.state.title = metadata.title
|
||||
await Session.updatePart(toolPart)
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
assistantMsg.time.completed = Date.now()
|
||||
await Session.updateMessage(assistantMsg)
|
||||
if (toolPart.state.status === "running") {
|
||||
toolPart.state = {
|
||||
status: "completed",
|
||||
time: {
|
||||
...toolPart.state.time,
|
||||
end: Date.now(),
|
||||
},
|
||||
input: toolPart.state.input,
|
||||
title: "",
|
||||
metadata: taskResult.metadata,
|
||||
output: taskResult.output,
|
||||
}
|
||||
await Session.updatePart(toolPart)
|
||||
}
|
||||
|
||||
result = { info: assistantMsg, parts: [toolPart] }
|
||||
} else {
|
||||
result = (await prompt({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
model,
|
||||
agent: agentName,
|
||||
parts,
|
||||
})) as MessageV2.WithParts
|
||||
parts.push({
|
||||
type: "agent",
|
||||
name: agent.name,
|
||||
})
|
||||
}
|
||||
|
||||
const result = (await prompt({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
model,
|
||||
agent: agentName,
|
||||
parts,
|
||||
})) as MessageV2.WithParts
|
||||
|
||||
Bus.publish(Command.Event.Executed, {
|
||||
name: input.command,
|
||||
sessionID: input.sessionID,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Log } from "../util/log"
|
|||
import { splitWhen } from "remeda"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Bus } from "../bus"
|
||||
import { SessionLock } from "./lock"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
|
||||
export namespace SessionRevert {
|
||||
const log = Log.create({ service: "session.revert" })
|
||||
|
|
@ -20,11 +20,7 @@ export namespace SessionRevert {
|
|||
export type RevertInput = z.infer<typeof RevertInput>
|
||||
|
||||
export async function revert(input: RevertInput) {
|
||||
SessionLock.assertUnlocked(input.sessionID)
|
||||
using _ = SessionLock.acquire({
|
||||
sessionID: input.sessionID,
|
||||
})
|
||||
|
||||
SessionPrompt.assertNotBusy(input.sessionID)
|
||||
const all = await Session.messages({ sessionID: input.sessionID })
|
||||
let lastUser: MessageV2.User | undefined
|
||||
const session = await Session.get(input.sessionID)
|
||||
|
|
@ -70,10 +66,7 @@ export namespace SessionRevert {
|
|||
|
||||
export async function unrevert(input: { sessionID: string }) {
|
||||
log.info("unreverting", input)
|
||||
SessionLock.assertUnlocked(input.sessionID)
|
||||
using _ = SessionLock.acquire({
|
||||
sessionID: input.sessionID,
|
||||
})
|
||||
SessionPrompt.assertNotBusy(input.sessionID)
|
||||
const session = await Session.get(input.sessionID)
|
||||
if (!session.revert) return session
|
||||
if (session.revert.snapshot) await Snapshot.restore(session.revert.snapshot)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { Bus } from "../bus"
|
|||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { SessionLock } from "../session/lock"
|
||||
import { SessionPrompt } from "../session/prompt"
|
||||
|
||||
export const TaskTool = Tool.define("task", async () => {
|
||||
|
|
@ -63,7 +62,7 @@ export const TaskTool = Tool.define("task", async () => {
|
|||
}
|
||||
|
||||
ctx.abort.addEventListener("abort", () => {
|
||||
SessionLock.abort(session.id)
|
||||
SessionPrompt.cancel(session.id)
|
||||
})
|
||||
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
|
||||
const result = await SessionPrompt.prompt({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue