Skip to content

Commit 8d21e42

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@f17c5a3dcc2cb06de23909c91f45b963a28ba443
1 parent 5454338 commit 8d21e42

3 files changed

Lines changed: 55 additions & 5 deletions

File tree

bun.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/constants/freebuff-errors.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ export const FREEBUFF_PROVIDER_USAGE_MESSAGE =
99
/**
1010
* The completions API's per-turn spend breaker (web/src/app/api/v1/chat/
1111
* completions/_post.ts, packages/billing/src/freebuff-turn-spend.ts): a 429
12-
* whose body is `{ error: 'turn_spend_limit', message }`. It is not a quota
13-
* and not transient — the same turn is refused again on every retry, because
12+
* whose body is `{ error: 'turn_spend_limit', message }`. It is not transient
13+
* — the same turn is refused again on every retry, because
1414
* its spend only ever grows — so every client stops retrying on sight and
1515
* shows the message as-is. A NEW message starts a new turn with a fresh
1616
* budget, which is what the copy says.
1717
*/
1818
export const FREEBUFF_TURN_SPEND_LIMIT_ERROR_CODE = 'turn_spend_limit'
1919

2020
export const FREEBUFF_TURN_SPEND_LIMIT_MESSAGE =
21-
'Something went wrong with this turn — it kept accumulating model usage well past what a single turn should use (this usually means an agent got stuck in a loop). Your session is fine: send a new message to continue from here.'
21+
'This turn reached its model usage limit. Your session is still available — send a new message to continue from here.'

packages/agent-runtime/src/__tests__/loop-agent-steps.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as analytics from '@codebuff/common/analytics'
2+
import { FREEBUFF_TURN_SPEND_LIMIT_MESSAGE } from '@codebuff/common/constants/freebuff-errors'
23
import { TEST_USER_ID } from '@codebuff/common/old-constants'
34
import { createTestAgentRuntimeParams } from '@codebuff/common/testing/fixtures/agent-runtime'
45
import { clearMockedModules } from '@codebuff/common/testing/mock-modules'
@@ -157,6 +158,57 @@ describe('loopAgentSteps - runAgentStep vs runProgrammaticStep behavior', () =>
157158
clearMockedModules()
158159
})
159160

161+
it('retains completed steps after a spending cap and resumes under a new run id', async () => {
162+
mockTemplate.handleSteps = function* () {
163+
yield 'STEP'
164+
yield 'STEP'
165+
}
166+
let calls = 0
167+
const capped = await loopAgentSteps({
168+
...loopAgentStepsBaseParams,
169+
startAgentRun: async () => 'capped-run',
170+
promptAiSdkStream: async function* () {
171+
if (++calls === 2) {
172+
throw new APICallError({
173+
statusCode: 429,
174+
message: FREEBUFF_TURN_SPEND_LIMIT_MESSAGE,
175+
url: 'http://localhost/api/v1/chat/completions',
176+
requestBodyValues: {},
177+
responseBody: JSON.stringify({
178+
error: 'turn_spend_limit',
179+
message: FREEBUFF_TURN_SPEND_LIMIT_MESSAGE,
180+
}),
181+
isRetryable: false,
182+
})
183+
}
184+
yield { type: 'text' as const, text: 'Completed research: result 42.' }
185+
yield createToolCallChunk('end_turn', {})
186+
return promptSuccess('completed-step')
187+
},
188+
})
189+
expect(calls).toBe(2)
190+
expect(capped.output).toMatchObject({
191+
type: 'error',
192+
error: 'turn_spend_limit',
193+
})
194+
expect(JSON.stringify(capped.agentState.messageHistory)).toContain(
195+
'Completed research: result 42.',
196+
)
197+
expect(capped.agentState.runId).toBe('capped-run')
198+
199+
const resumed = await loopAgentSteps({
200+
...loopAgentStepsBaseParams,
201+
agentState: capped.agentState,
202+
prompt: 'Continue',
203+
startAgentRun: async () => 'fresh-run',
204+
})
205+
expect(resumed.output.type).not.toBe('error')
206+
expect(resumed.agentState.runId).toBe('fresh-run')
207+
const history = JSON.stringify(resumed.agentState.messageHistory)
208+
expect(history).toContain('Completed research: result 42.')
209+
expect(history).toContain('Continue')
210+
})
211+
160212
it('an abort during agent-run registration is a cancel, not a failed run', async () => {
161213
// registration ends on the run's signal now, so the null it returns under an abort is the
162214
// abort itself and must read like every other cancel

0 commit comments

Comments
 (0)