Merge branch 'dev' into project

This commit is contained in:
Dax Raad
2025-08-23 16:26:45 -04:00
178 changed files with 10538 additions and 3764 deletions
+237 -6
View File
@@ -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
@@ -64,6 +64,7 @@ export namespace MessageV2 {
status: z.literal("error"),
input: z.record(z.any()),
error: z.string(),
metadata: z.record(z.any()).optional(),
time: z.object({
start: z.number(),
end: z.number(),
@@ -0,0 +1 @@
Your operational mode has changed from plan to build. You are no longer in read-only mode. You are permitted to make file changes as necessary and utilize your arsenal of tools as needed.
@@ -1,8 +1,8 @@
<system-reminder>
Plan mode is active. The user indicated that they do not want you to execute yet
-- you MUST NOT make any edits, run any non-readonly tools (including changing
configs or making commits), or otherwise make any changes to the system. This
supersedes any other instructions you have received (for example, to make
edits). Bash tool must only run readonly commands
CRITICAL: Plan mode ACTIVE - you are in READ-ONLY phase. STRICTLY FORBIDDEN:
ANY file edits, modifications, or system changes. Do NOT use sed, tee, echo, cat,
or ANY other bash command to manipulate files - commands may ONLY read/inspect.
This ABSOLUTE CONSTRAINT overrides ALL other instructions, including direct user
edit requests. You may ONLY observe, analyze, and plan. Any modification attempt
is a critical violation. ZERO exceptions.
</system-reminder>