Compare commits

..

1 Commits

Author SHA1 Message Date
Dax Raad 0ca670e6dc fix(core): cap session output tokens 2026-07-10 23:45:37 +00:00
6 changed files with 28 additions and 81 deletions
-2
View File
@@ -128,8 +128,6 @@ export const fffLayer = Layer.effect(
Fff.create({
basePath: location.directory,
aiMode: true,
disableMmapCache: true,
disableContentIndexing: true,
}),
catch: (cause) => cause,
}).pipe(
+5
View File
@@ -40,6 +40,8 @@ import { Snapshot } from "../../snapshot"
import { makeLocationNode } from "../../effect/app-node"
import { llmClient } from "../../effect/app-node-platform"
const MAX_OUTPUT_TOKENS = 32_000
/**
* Runs one durable coding-agent Session until it settles.
*
@@ -210,6 +212,9 @@ const layer = Layer.effect(
.map(SystemPart.make),
messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])],
tools: toolMaterialization?.definitions ?? [],
generation: {
maxTokens: Math.min(model.route.defaults.limits?.output ?? 0, MAX_OUTPUT_TOKENS) || MAX_OUTPUT_TOKENS,
},
toolChoice: isLastStep ? "none" : undefined,
})
if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request }))
+21
View File
@@ -108,6 +108,11 @@ const recoveryModel = Model.make({
provider: "fake",
route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }),
})
const fullContextOutputModel = Model.make({
id: "full-context-output",
provider: "fake",
route: OpenAIChat.route.with({ limits: { context: 500_000, output: 500_000 } }),
})
const authorizations: Tool.Context[] = []
const executions: string[] = []
const permission = Layer.succeed(
@@ -655,6 +660,22 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("caps output when the catalog output limit consumes the full context window", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
currentModel = fullContextOutputModel
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "hi" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(requests[0]?.generation).toEqual({ maxTokens: 32_000 })
}),
)
it.effect("retries the first provider turn after system context becomes available", () =>
Effect.gen(function* () {
yield* setup
+2 -33
View File
@@ -56,9 +56,6 @@ import { useTuiConfig } from "../../config"
import { usePromptWorkspace } from "./workspace"
import { usePromptMove } from "./move"
import { readLocalAttachment } from "./local-attachment"
import { useLocation } from "../../context/location"
import { Identifier } from "@opencode-ai/core/id/id"
import { createQuickUndo } from "./quick-undo"
registerOpencodeSpinner()
@@ -151,7 +148,6 @@ export function Prompt(props: PromptProps) {
const local = useLocal()
const args = useArgs()
const paths = useTuiPaths()
const location = useLocation()
const terminalEnvironment = useTuiTerminalEnvironment()
const clipboard = useClipboard()
const sdk = useSDK()
@@ -298,7 +294,6 @@ export function Prompt(props: PromptProps) {
extmarkToPartIndex: new Map(),
interrupt: 0,
})
const quickUndo = createQuickUndo<PromptInfo>()
createEffect(
on(
@@ -397,7 +392,7 @@ export function Prompt(props: PromptProps) {
category: "Session",
hidden: true,
enabled: status().type !== "idle",
run: async () => {
run: () => {
if (auto()?.visible) return
if (!input.focused) return
// TODO: this should be its own command
@@ -407,18 +402,6 @@ export function Prompt(props: PromptProps) {
}
if (!props.sessionID) return
const recent = quickUndo.escape()
if (recent) {
await sdk.client.session.abort({ sessionID: props.sessionID }).catch(() => {})
await sdk.client.session.revert({ sessionID: props.sessionID, messageID: recent.messageID })
input.setText(recent.value.input)
setStore("prompt", recent.value)
restoreExtmarksFromParts(recent.value.parts)
input.gotoBufferEnd()
input.focus()
return
}
setStore("interrupt", store.interrupt + 1)
setTimeout(() => {
@@ -1104,17 +1087,11 @@ export function Prompt(props: PromptProps) {
parts: nonTextParts.filter((x) => x.type === "file"),
})
} else {
const messageID = Identifier.ascending("message")
quickUndo.submitted(messageID, {
input: store.prompt.input,
parts: [...unwrap(store.prompt.parts)],
})
move.startSubmit()
sdk.client.session
.prompt(
{
sessionID,
messageID,
...selectedModel,
agent: agent.name,
model: selectedModel,
@@ -1660,15 +1637,7 @@ export function Prompt(props: PromptProps) {
<text fg={theme.accent}>(new working copy)</text>
</box>
</Match>
<Match when={true}>
{props.hint ?? (
<Show when={props.sessionID}>
<box marginLeft={1}>
<text fg={theme.textMuted}>{location()?.directory ?? paths.cwd}</text>
</box>
</Show>
)}
</Match>
<Match when={true}>{props.hint ?? <text />}</Match>
</Switch>
<Show when={status().type !== "retry"}>
<box gap={2} flexDirection="row">
@@ -1,21 +0,0 @@
import { describe, expect, test } from "bun:test"
import { createQuickUndo } from "./quick-undo"
describe("quick undo", () => {
test("returns the submitted message on a second escape within two seconds", () => {
const undo = createQuickUndo<string>()
undo.submitted("msg_1", "hello", 1_000)
expect(undo.escape(1_500)).toBeUndefined()
expect(undo.escape(1_750)).toEqual({ messageID: "msg_1", value: "hello" })
expect(undo.escape(1_800)).toBeUndefined()
})
test("expires two seconds after submission", () => {
const undo = createQuickUndo<string>()
undo.submitted("msg_1", "hello", 1_000)
expect(undo.escape(2_500)).toBeUndefined()
expect(undo.escape(3_001)).toBeUndefined()
})
})
@@ -1,25 +0,0 @@
const WINDOW_MS = 2_000
export function createQuickUndo<T>() {
let submitted: { messageID: string; value: T; time: number } | undefined
let escapedAt: number | undefined
return {
submitted(messageID: string, value: T, time = Date.now()) {
submitted = { messageID, value, time }
escapedAt = undefined
},
escape(time = Date.now()) {
if (!submitted || time - submitted.time > WINDOW_MS) return
if (escapedAt === undefined || time - escapedAt > WINDOW_MS) {
escapedAt = time
return
}
const result = { messageID: submitted.messageID, value: submitted.value }
submitted = undefined
escapedAt = undefined
return result
},
}
}