Skip to content

Commit ac2391d

Browse files
KyleAMathewssamwilliscursoragent
authored
Capture Codex reasoning summaries (#4617)
## Summary - request OpenAI/Codex reasoning summaries by default when enabling reasoning effort - preserve explicit reasoning summary settings from caller payloads - add adapter coverage proving streamed thinking events are written as entity reasoning rows/deltas ## Verification - cd packages/agents-runtime && pnpm exec vitest run test/pi-adapter.test.ts - cd packages/agents && pnpm exec vitest run test/model-catalog.test.ts --------- Co-authored-by: Sam Willis <sam.willis@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8f4368d commit ac2391d

4 files changed

Lines changed: 213 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@electric-ax/agents': patch
3+
'@electric-ax/agents-runtime': patch
4+
---
5+
6+
Capture OpenAI Codex reasoning summaries by default when built-in reasoning is enabled while preserving explicit reasoning summary settings from caller payloads.

packages/agents-runtime/test/pi-adapter.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,145 @@ describe(`createPiAgentAdapter`, () => {
689689
)
690690
})
691691

692+
it(`writes reasoning rows for streamed thinking events`, async () => {
693+
const events: Array<ChangeEvent> = []
694+
const usage = {
695+
input: 0,
696+
output: 0,
697+
cacheRead: 0,
698+
cacheWrite: 0,
699+
totalTokens: 0,
700+
cost: {
701+
input: 0,
702+
output: 0,
703+
cacheRead: 0,
704+
cacheWrite: 0,
705+
total: 0,
706+
},
707+
}
708+
const startMessage: AssistantMessage = {
709+
role: `assistant`,
710+
content: [],
711+
api: `openai-codex-responses`,
712+
provider: `openai-codex`,
713+
model: `gpt-5.4`,
714+
usage,
715+
stopReason: `stop`,
716+
timestamp: Date.now(),
717+
}
718+
const partialMessage: AssistantMessage = {
719+
...startMessage,
720+
content: [{ type: `thinking`, thinking: `**Updating PR**\n\n` }],
721+
}
722+
const finalMessage: AssistantMessage = {
723+
...startMessage,
724+
content: [
725+
{
726+
type: `thinking`,
727+
thinking: `**Updating PR**\n\nI'm updating the PR description.`,
728+
},
729+
],
730+
}
731+
732+
const factory = createPiAgentAdapter({
733+
systemPrompt: `Test system prompt`,
734+
provider: `openai-codex`,
735+
model: `gpt-5.4`,
736+
tools: [],
737+
streamFn: () => {
738+
const stream = createAssistantMessageEventStream()
739+
queueMicrotask(() => {
740+
stream.push({ type: `start`, partial: startMessage })
741+
stream.push({
742+
type: `thinking_start`,
743+
contentIndex: 0,
744+
partial: partialMessage,
745+
})
746+
stream.push({
747+
type: `thinking_delta`,
748+
contentIndex: 0,
749+
delta: `**Updating PR**\n\n`,
750+
partial: partialMessage,
751+
})
752+
stream.push({
753+
type: `thinking_delta`,
754+
contentIndex: 0,
755+
delta: `I'm updating the PR description.`,
756+
partial: finalMessage,
757+
})
758+
stream.push({
759+
type: `thinking_end`,
760+
contentIndex: 0,
761+
content: `**Updating PR**\n\nI'm updating the PR description.`,
762+
partial: finalMessage,
763+
})
764+
stream.end(finalMessage)
765+
})
766+
return stream
767+
},
768+
})
769+
770+
const handle = factory({
771+
entityUrl: `test/entity-1`,
772+
epoch: 1,
773+
messages: [],
774+
outboundIdSeed: { run: 0, step: 0, msg: 0, tc: 0, reasoning: 0 },
775+
writeEvent: (event: ChangeEvent) => {
776+
events.push(event)
777+
},
778+
})
779+
780+
await handle.run(`hello`)
781+
782+
expect(events).toContainEqual(
783+
expect.objectContaining({
784+
type: `reasoning`,
785+
headers: expect.objectContaining({ operation: `insert` }),
786+
key: `reasoning-0`,
787+
value: expect.objectContaining({
788+
status: `streaming`,
789+
run_id: `run-0`,
790+
}),
791+
})
792+
)
793+
expect(events).toContainEqual(
794+
expect.objectContaining({
795+
type: `reasoning_delta`,
796+
headers: expect.objectContaining({ operation: `insert` }),
797+
key: `reasoning-0:0`,
798+
value: expect.objectContaining({
799+
reasoning_id: `reasoning-0`,
800+
run_id: `run-0`,
801+
delta: `**Updating PR**\n\n`,
802+
}),
803+
})
804+
)
805+
expect(events).toContainEqual(
806+
expect.objectContaining({
807+
type: `reasoning_delta`,
808+
headers: expect.objectContaining({ operation: `insert` }),
809+
key: `reasoning-0:1`,
810+
value: expect.objectContaining({
811+
reasoning_id: `reasoning-0`,
812+
run_id: `run-0`,
813+
delta: `I'm updating the PR description.`,
814+
}),
815+
})
816+
)
817+
expect(events).toContainEqual(
818+
expect.objectContaining({
819+
type: `reasoning`,
820+
headers: expect.objectContaining({ operation: `update` }),
821+
key: `reasoning-0`,
822+
value: expect.objectContaining({
823+
status: `completed`,
824+
run_id: `run-0`,
825+
summary_title: `Updating PR`,
826+
}),
827+
})
828+
)
829+
})
830+
692831
it(`isRunning returns false initially`, () => {
693832
const factory = createPiAgentAdapter({
694833
systemPrompt: `Test system prompt`,

packages/agents/src/model-catalog.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,37 @@ function resolveBuiltinReasoning(
270270
return reasoningEffort ?? `minimal`
271271
}
272272

273+
function resolveBuiltinPayloadMapper(
274+
choice: BuiltinModelChoice,
275+
reasoning: AgentConfig[`reasoning`] | undefined
276+
): AgentConfig[`onPayload`] | undefined {
277+
if (
278+
!choice.reasoning ||
279+
(choice.provider !== `openai` && choice.provider !== `openai-codex`) ||
280+
!reasoning
281+
) {
282+
return undefined
283+
}
284+
285+
return (payload) => {
286+
if (typeof payload !== `object` || payload === null) return undefined
287+
const body = payload as Record<string, unknown>
288+
const existingReasoning =
289+
typeof body.reasoning === `object` && body.reasoning !== null
290+
? (body.reasoning as Record<string, unknown>)
291+
: {}
292+
293+
return {
294+
...body,
295+
reasoning: {
296+
...existingReasoning,
297+
effort: reasoning,
298+
summary: existingReasoning.summary ?? `auto`,
299+
},
300+
}
301+
}
302+
}
303+
273304
function parseReasoningEffort(value: unknown): ExplicitReasoningEffort | null {
274305
return value === `minimal` ||
275306
value === `low` ||
@@ -345,11 +376,13 @@ export function resolveBuiltinModelConfig(
345376

346377
const choice = selected ?? catalog.defaultChoice
347378
const reasoning = resolveBuiltinReasoning(choice, reasoningEffort)
379+
const onPayload = resolveBuiltinPayloadMapper(choice, reasoning)
348380
const config: BuiltinAgentModelConfig = {
349381
provider: choice.provider,
350382
model: choice.id,
351383
...(reasoningEffort && { reasoningEffort }),
352384
...(reasoning && { reasoning }),
385+
...(onPayload && { onPayload }),
353386
...(choice.reasoning &&
354387
choice.provider === `anthropic` && {
355388
thinkingBudgets: ANTHROPIC_THINKING_BUDGETS,

packages/agents/test/model-catalog.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,16 @@ describe(`model catalog`, () => {
119119
model: `gpt-5`,
120120
reasoning: `minimal`,
121121
})
122-
expect(config.onPayload).toBeUndefined()
122+
expect(config.onPayload).toBeTypeOf(`function`)
123+
124+
const payload = config.onPayload!(
125+
{ reasoning: { effort: `none` } },
126+
{} as any
127+
)
128+
129+
expect(payload).toEqual({
130+
reasoning: { effort: `minimal`, summary: `auto` },
131+
})
123132
})
124133

125134
it(`uses explicit reasoning effort for OpenAI reasoning models`, async () => {
@@ -131,6 +140,31 @@ describe(`model catalog`, () => {
131140

132141
expect(config.reasoningEffort).toBe(`high`)
133142
expect(config.reasoning).toBe(`high`)
143+
144+
const payload = config.onPayload!(
145+
{ reasoning: { effort: `none` } },
146+
{} as any
147+
)
148+
149+
expect(payload).toEqual({
150+
reasoning: { effort: `high`, summary: `auto` },
151+
})
152+
})
153+
154+
it(`preserves an explicit OpenAI reasoning summary setting`, async () => {
155+
const catalog = await createBuiltinModelCatalog()
156+
const config = resolveBuiltinModelConfig(catalog!, {
157+
model: `openai:gpt-5`,
158+
})
159+
160+
const payload = config.onPayload!(
161+
{ reasoning: { effort: `none`, summary: `detailed` } },
162+
{} as any
163+
)
164+
165+
expect(payload).toEqual({
166+
reasoning: { effort: `minimal`, summary: `detailed` },
167+
})
134168
})
135169

136170
it(`enables Anthropic reasoning through pi-ai when reasoningEffort is auto`, async () => {

0 commit comments

Comments
 (0)