Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/opencode/src/effect/runtime-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
experimentalEventSystem: enabledByExperimental("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),
experimentalWorkspaces: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"),
experimentalLengthNudge: enabledByExperimental("OPENCODE_EXPERIMENTAL_LENGTH_NUDGE"),
lengthNudgeMax: positiveInteger("OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_MAX"),
lengthNudgePrompt: Config.string("OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_PROMPT").pipe(
Config.withDefault(
"Your previous response was cut off by the output token limit before it finished. Continue the task from where you stopped. Do not repeat work that is already complete.",
),
),
outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
experimentalNativeLlm: bool("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
Expand Down
43 changes: 43 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,7 @@ const layer = Layer.effect(
const ctx = yield* InstanceState.context
let structured: unknown
let step = 0
let lengthNudges = 0
const session = yield* sessions.get(sessionID).pipe(Effect.orDie)

while (true) {
Expand All @@ -1107,6 +1108,8 @@ const layer = Layer.effect(
lastAssistantMsg?.parts.some(
(part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),
) ?? false
const hasVisibleText =
lastAssistantMsg?.parts.some((part) => part.type === "text" && part.text.trim().length > 0) ?? false

if (
lastAssistant?.finish &&
Expand All @@ -1125,6 +1128,46 @@ const layer = Layer.effect(
callID: orphan.callID,
})
}

if (
flags.experimentalLengthNudge &&
lastAssistant.finish === "length" &&
!hasVisibleText &&
lengthNudges < (flags.lengthNudgeMax ?? 3)
) {
lengthNudges++
yield* Effect.logWarning("nudging length-truncated turn", {
"session.id": sessionID,
messageID: lastAssistant.id,
attempt: lengthNudges,
max: flags.lengthNudgeMax ?? 3,
})
const nudge: SessionV1.User = {
id: MessageID.ascending(),
role: "user",
sessionID,
time: { created: Date.now() },
agent: lastUser.agent,
model: lastUser.model,
}
yield* sessions.updateMessage(nudge)
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: nudge.id,
sessionID,
type: "text",
text: flags.lengthNudgePrompt,
synthetic: true,
})
continue
}
if (lastAssistant.finish === "length" && flags.experimentalLengthNudge && lengthNudges >= (flags.lengthNudgeMax ?? 3)) {
yield* Effect.logWarning("exiting loop after max length nudges", {
"session.id": sessionID,
messageID: lastAssistant.id,
nudges: lengthNudges,
})
}
yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
break
}
Expand Down
81 changes: 81 additions & 0 deletions packages/opencode/test/effect/runtime-flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,87 @@ describe("RuntimeFlags", () => {
}),
)

it.effect("experimentalLengthNudge defaults to false", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))

expect(flags.experimentalLengthNudge).toBe(false)
}),
)

it.effect("experimentalLengthNudge reads OPENCODE_EXPERIMENTAL_LENGTH_NUDGE", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(
Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_LENGTH_NUDGE: "true" })),
)

expect(flags.experimentalLengthNudge).toBe(true)
}),
)

it.effect("experimentalLengthNudge inherits OPENCODE_EXPERIMENTAL", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(
Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" })),
)

expect(flags.experimentalLengthNudge).toBe(true)
}),
)

for (const input of [
{ name: "absent", config: {}, expected: undefined },
{
name: "valid positive integer",
config: { OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_MAX: "5" },
expected: 5,
},
{
name: "invalid string",
config: { OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_MAX: "nope" },
expected: undefined,
},
{ name: "zero", config: { OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_MAX: "0" }, expected: undefined },
{
name: "negative",
config: { OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_MAX: "-1" },
expected: undefined,
},
{
name: "non-integer",
config: { OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_MAX: "1.5" },
expected: undefined,
},
]) {
it.effect(`parses lengthNudgeMax from config: ${input.name}`, () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(Effect.provide(fromConfig(input.config)))

expect(flags.lengthNudgeMax).toBe(input.expected)
}),
)
}

it.effect("lengthNudgePrompt defaults to the standard nudge text", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))

expect(flags.lengthNudgePrompt).toBe(
"Your previous response was cut off by the output token limit before it finished. Continue the task from where you stopped. Do not repeat work that is already complete.",
)
}),
)

it.effect("lengthNudgePrompt reads OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_PROMPT", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(
Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_LENGTH_NUDGE_PROMPT: "Keep going." })),
)

expect(flags.lengthNudgePrompt).toBe("Keep going.")
}),
)

for (const input of [
{ name: "absent", config: {}, expected: undefined },
{
Expand Down
Loading