Merge branch 'dev' into project
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import path from "path"
|
||||
import { spawn } from "child_process"
|
||||
import { Decimal } from "decimal.js"
|
||||
import { z, ZodSchema } from "zod"
|
||||
import {
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
|
||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
||||
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
||||
|
||||
import { Bus } from "../bus"
|
||||
import { Config } from "../config/config"
|
||||
@@ -43,6 +46,11 @@ import { Instance } from "../project/instance"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Permission } from "../permission"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { ulid } from "ulid"
|
||||
import { defer } from "../util/defer"
|
||||
import { Command } from "../command"
|
||||
import { $ } from "bun"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -157,14 +165,15 @@ export namespace Session {
|
||||
},
|
||||
)
|
||||
|
||||
export async function create(parentID?: string) {
|
||||
export async function create(parentID?: string, title?: string) {
|
||||
return createNext({
|
||||
parentID,
|
||||
directory: Instance.directory,
|
||||
title,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createNext(input: { id?: string; parentID?: string; directory: string }) {
|
||||
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
|
||||
const project = Project.use()
|
||||
const result: Info = {
|
||||
id: Identifier.descending("session", input.id),
|
||||
@@ -172,7 +181,7 @@ export namespace Session {
|
||||
projectID: project.id,
|
||||
directory: input.directory,
|
||||
parentID: input.parentID,
|
||||
title: createDefaultTitle(!!input.parentID),
|
||||
title: input.title ?? createDefaultTitle(!!input.parentID),
|
||||
time: {
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
@@ -528,6 +537,7 @@ export namespace Session {
|
||||
abort: new AbortController().signal,
|
||||
agent: input.agent!,
|
||||
messageID: userMsg.id,
|
||||
extra: { bypassCwdCheck: true },
|
||||
metadata: async () => {},
|
||||
}),
|
||||
)
|
||||
@@ -670,7 +680,7 @@ export namespace Session {
|
||||
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
|
||||
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
|
||||
|
||||
if (msgs.length === 1 && !session.parentID && isDefaultTitle(session.title)) {
|
||||
if (msgs.filter((m) => m.info.role === "user").length === 1 && !session.parentID && isDefaultTitle(session.title)) {
|
||||
const small = (await Provider.getSmallModel(input.providerID)) ?? model
|
||||
generateText({
|
||||
maxOutputTokens: small.info.reasoning ? 1024 : 20,
|
||||
@@ -725,6 +735,18 @@ export namespace Session {
|
||||
synthetic: true,
|
||||
})
|
||||
}
|
||||
|
||||
const lastAssistantMsg = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
|
||||
if (lastAssistantMsg?.mode === "plan" && agent.name === "build") {
|
||||
msgs.at(-1)?.parts.push({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text: BUILD_SWITCH,
|
||||
synthetic: true,
|
||||
})
|
||||
}
|
||||
let system = SystemPrompt.header(input.providerID)
|
||||
system.push(
|
||||
...(() => {
|
||||
@@ -763,6 +785,11 @@ export namespace Session {
|
||||
sessionID: input.sessionID,
|
||||
}
|
||||
await updateMessage(assistantMsg)
|
||||
await using _ = defer(async () => {
|
||||
if (assistantMsg.time.completed) return
|
||||
await StorageNext.remove(["session", "message", input.sessionID, assistantMsg.id])
|
||||
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: assistantMsg.id })
|
||||
})
|
||||
const tools: Record<string, AITool> = {}
|
||||
|
||||
const processor = createProcessor(assistantMsg, model.info)
|
||||
@@ -939,6 +966,13 @@ export namespace Session {
|
||||
toolName: "invalid",
|
||||
}
|
||||
},
|
||||
headers:
|
||||
input.providerID === "opencode"
|
||||
? {
|
||||
"x-opencode-session": input.sessionID,
|
||||
"x-opencode-request": userMsg.id,
|
||||
}
|
||||
: undefined,
|
||||
maxRetries: 3,
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
maxOutputTokens: outputLimit,
|
||||
@@ -967,7 +1001,7 @@ export namespace Session {
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...MessageV2.toModelMessage(msgs),
|
||||
...MessageV2.toModelMessage(msgs.filter((m) => !(m.info.role === "assistant" && m.info.error))),
|
||||
],
|
||||
tools: model.info.tool_call === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
@@ -999,6 +1033,202 @@ export namespace Session {
|
||||
return result
|
||||
}
|
||||
|
||||
export const ShellInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
agent: z.string(),
|
||||
command: z.string(),
|
||||
})
|
||||
export type ShellInput = z.infer<typeof ShellInput>
|
||||
export async function shell(input: ShellInput) {
|
||||
using abort = lock(input.sessionID)
|
||||
const msg: MessageV2.Assistant = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
system: [],
|
||||
mode: input.agent,
|
||||
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: "",
|
||||
providerID: "",
|
||||
}
|
||||
await updateMessage(msg)
|
||||
const part: MessageV2.Part = {
|
||||
type: "tool",
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: msg.id,
|
||||
sessionID: input.sessionID,
|
||||
tool: "bash",
|
||||
callID: ulid(),
|
||||
state: {
|
||||
status: "running",
|
||||
time: {
|
||||
start: Date.now(),
|
||||
},
|
||||
input: {
|
||||
command: input.command,
|
||||
},
|
||||
},
|
||||
}
|
||||
await updatePart(part)
|
||||
const app = App.info()
|
||||
const shell = process.env["SHELL"] ?? "bash"
|
||||
const shellName = path.basename(shell)
|
||||
|
||||
const scripts: Record<string, string> = {
|
||||
nu: input.command,
|
||||
fish: `eval "${input.command}"`,
|
||||
}
|
||||
|
||||
const script =
|
||||
scripts[shellName] ??
|
||||
`[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
|
||||
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
|
||||
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
|
||||
eval "${input.command}"`
|
||||
|
||||
const isFishOrNu = shellName === "fish" || shellName === "nu"
|
||||
const args = isFishOrNu ? ["-c", script] : ["-c", "-l", script]
|
||||
|
||||
const proc = spawn(shell, args, {
|
||||
cwd: app.path.cwd,
|
||||
signal: abort.signal,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
TERM: "dumb",
|
||||
},
|
||||
})
|
||||
|
||||
let output = ""
|
||||
|
||||
proc.stdout?.on("data", (chunk) => {
|
||||
output += chunk.toString()
|
||||
if (part.state.status === "running") {
|
||||
part.state.metadata = {
|
||||
output: output,
|
||||
description: "",
|
||||
}
|
||||
updatePart(part)
|
||||
}
|
||||
})
|
||||
|
||||
proc.stderr?.on("data", (chunk) => {
|
||||
output += chunk.toString()
|
||||
if (part.state.status === "running") {
|
||||
part.state.metadata = {
|
||||
output: output,
|
||||
description: "",
|
||||
}
|
||||
updatePart(part)
|
||||
}
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
proc.on("close", () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
msg.time.completed = Date.now()
|
||||
await updateMessage(msg)
|
||||
if (part.state.status === "running") {
|
||||
part.state = {
|
||||
status: "completed",
|
||||
time: {
|
||||
...part.state.time,
|
||||
end: Date.now(),
|
||||
},
|
||||
input: part.state.input,
|
||||
title: "",
|
||||
metadata: {
|
||||
output,
|
||||
description: "",
|
||||
},
|
||||
output,
|
||||
}
|
||||
await updatePart(part)
|
||||
}
|
||||
return { info: msg, parts: [part] }
|
||||
}
|
||||
|
||||
export const CommandInput = z.object({
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
sessionID: Identifier.schema("session"),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
arguments: z.string(),
|
||||
command: z.string(),
|
||||
})
|
||||
export type CommandInput = z.infer<typeof CommandInput>
|
||||
const bashRegex = /!`([^`]+)`/g
|
||||
const fileRegex = /@([^\s]+)/g
|
||||
|
||||
export async function command(input: CommandInput) {
|
||||
const command = await Command.get(input.command)
|
||||
const agent = input.agent ?? command.agent ?? "build"
|
||||
const model =
|
||||
input.model ??
|
||||
command.model ??
|
||||
(await Agent.get(agent).then((x) => (x.model ? `${x.model.providerID}/${x.model.modelID}` : undefined))) ??
|
||||
(await Provider.defaultModel().then((x) => `${x.providerID}/${x.modelID}`))
|
||||
let template = command.template.replace("$ARGUMENTS", input.arguments)
|
||||
|
||||
const bash = Array.from(template.matchAll(bashRegex))
|
||||
if (bash.length > 0) {
|
||||
const results = await Promise.all(
|
||||
bash.map(async ([, cmd]) => {
|
||||
try {
|
||||
return await $`${{ raw: cmd }}`.nothrow().text()
|
||||
} catch (error) {
|
||||
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}),
|
||||
)
|
||||
let index = 0
|
||||
template = template.replace(bashRegex, () => results[index++])
|
||||
}
|
||||
|
||||
const parts = [
|
||||
{
|
||||
type: "text",
|
||||
text: template,
|
||||
},
|
||||
] as ChatInput["parts"]
|
||||
|
||||
const matches = template.matchAll(fileRegex)
|
||||
const app = App.info()
|
||||
|
||||
for (const match of matches) {
|
||||
const file = path.join(app.path.cwd, match[1])
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: `file://${file}`,
|
||||
filename: match[1],
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
|
||||
return chat({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
...Provider.parseModel(model!),
|
||||
agent,
|
||||
parts,
|
||||
})
|
||||
}
|
||||
|
||||
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
|
||||
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
||||
let snapshot: string | undefined
|
||||
@@ -1134,6 +1364,7 @@ export namespace Session {
|
||||
status: "error",
|
||||
input: value.input,
|
||||
error: (value.error as any).toString(),
|
||||
metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
|
||||
time: {
|
||||
start: match.state.time.start,
|
||||
end: Date.now(),
|
||||
@@ -1454,7 +1685,7 @@ export namespace Session {
|
||||
const tokens = {
|
||||
input: usage.inputTokens ?? 0,
|
||||
output: usage.outputTokens ?? 0,
|
||||
reasoning: 0,
|
||||
reasoning: usage?.reasoningTokens ?? 0,
|
||||
cache: {
|
||||
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||
// @ts-expect-error
|
||||
|
||||
Reference in New Issue
Block a user