Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b8b0da5
fix(a2a): reject encoded responses before body reads
markstuart-oai Nov 5, 2026
02aba47
fix(a2a): preserve serialized structured results
markstuart-oai Nov 5, 2026
ce6124e
Read the realtime pairing test request count from the token server
markstuart-oai Nov 5, 2026
e1a40fe
Merge the SessionStore runtime and allow the API coverage fixture to …
markstuart-oai Nov 5, 2026
1e311e3
chore: merge Codex package extraction into A2A response guard
markstuart-oai Nov 5, 2026
d16fcb3
Integrate the owned-child tracing fix with the A2A adapter
markstuart-oai Nov 5, 2026
dfbee78
chore: integrate current child tracing policy into A2A response guard
markstuart-oai Nov 5, 2026
61d83aa
Integrate the macOS executor HTTP probe fix
markstuart-oai Nov 5, 2026
b5c2b46
Retain the native workflow policy through queued child submissions
markstuart-oai Nov 5, 2026
b182783
Use the owning turn for executor approval tracing
markstuart-oai Nov 5, 2026
f367048
Assert the denied child command stops before executor dispatch
markstuart-oai Nov 5, 2026
8206687
Exercise independent ambient and instrumented trace contexts
markstuart-oai Nov 5, 2026
48aa8a1
Integrate native child tracing with A2A structured results
markstuart-oai Nov 5, 2026
39806a9
Merge child tracing isolation repair into A2A response encoding
markstuart-oai Nov 5, 2026
44f6e78
Preserve child callback ownership across queued submissions and execu…
markstuart-oai Nov 5, 2026
64bc380
Integrate native child callback ownership with A2A structured results
markstuart-oai Nov 5, 2026
82d821a
Merge current child callback ownership integration
markstuart-oai Nov 5, 2026
b723a82
Keep W3C trace ancestry separate from workflow export settings
markstuart-oai Nov 5, 2026
f58b643
Integrate native trace-policy isolation with A2A structured results
markstuart-oai Nov 5, 2026
d5b24dc
Carry the admitted caller context through native child operations
markstuart-oai Nov 5, 2026
2fec101
Preserve admitted caller context for A2A structured results
markstuart-oai Nov 5, 2026
05c403a
Bind cancellation to the admitted turn context
markstuart-oai Nov 5, 2026
6fcf2b3
Verify export routing through the public tracing API
markstuart-oai Nov 5, 2026
6d01bd0
fix: wait for settled native child admission
markstuart-oai Nov 5, 2026
1875314
Merge commit 'b8a51306ccceea0da5553703fef14a5afe3f8fb4' into codex/v1…
markstuart-oai Nov 5, 2026
c5971ad
fix: integrate queued tracing into response encoding
markstuart-oai Nov 5, 2026
a560d1e
fix: integrate response encoding and queued tracing into structured r…
markstuart-oai Nov 5, 2026
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
6 changes: 6 additions & 0 deletions .changeset/a2a-serialized-results.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@openai/agents': patch
'@openai/agents-codex': patch
---

fix: preserve the JSON representation of structured A2A results when saving and publishing a task.
2 changes: 1 addition & 1 deletion packages/agents-codex/A2A.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The returned client is the official A2A `Client`. Direct callers must treat an u

The provider requires separate callbacks for current authorization, credential resolution, durable mutation reservation, input encoding, and result validation. Authorization checks the current principal, immutable native invocation binding, and claim generation. After discovery and local validation, the provider resolves credentials, checks authorization again, spends one mutation reservation, and sends once. A timeout or lost create, input-reply, or cancel acknowledgment returns `unknown`. It never resends that mutation. A known receipt binds the endpoint and original native invocation. A fresh worker observes that same remote task with `getTask`; it does not create another task.

`inputRequired` exposes a remote challenge. `A2AInputReply` supplies its task ID, context ID, challenge, generation, and JSON `result`. The provider reads the current remote task before reserving a reply. Python and TypeScript share one wire profile: the status message contains one data part `{challenge, generation, tool, arguments}`; a reply contains exactly `{challenge, generation, result}`. Task and context IDs belong in the A2A message envelope. Structured completion artifacts contain `{result: output}`; string results remain text parts. `authRequired` remains a host credential flow and never becomes a generic approval answer. A host must not accept untrusted remote approval text as authority to perform local actions.
`inputRequired` exposes a remote challenge. `A2AInputReply` supplies its task ID, context ID, challenge, generation, and JSON `result`. The provider reads the current remote task before reserving a reply. Python and TypeScript share one wire profile: the status message contains one data part `{challenge, generation, tool, arguments}`; a reply contains exactly `{challenge, generation, result}`. Task and context IDs belong in the A2A message envelope. The executor publishes string results as text parts. For structured results, it saves and sends `{result: output}` using the JSON representation that passed the byte limit. `authRequired` remains a host credential flow and never becomes a generic approval answer. A host must not accept untrusted remote approval text as authority to perform local actions.

## Serve a native agent

Expand Down
3 changes: 2 additions & 1 deletion packages/agents-codex/src/a2a/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function bound(value: number | undefined, fallback: number): number {
return result;
}

export function checkBytes(value: unknown, limit: number): void {
export function checkBytes(value: unknown, limit: number): string {
let encoded: string | undefined;
try {
encoded = JSON.stringify(value);
Expand All @@ -81,6 +81,7 @@ export function checkBytes(value: unknown, limit: number): void {
}
if (encoded === undefined || Buffer.byteLength(encoded) > limit)
throw new A2AError('limit');
return encoded;
}

async function boundedResponse(
Expand Down
6 changes: 4 additions & 2 deletions packages/agents-codex/src/a2a/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ export class NativeA2AExecutor<
await session.close();
closed = true;
await claim.current();
checkBytes(
const encodedOutput = checkBytes(
outcome.finalOutput,
bound(this.#options.limits?.bodyBytes, 2 * 1024 * 1024),
);
Expand All @@ -417,7 +417,9 @@ export class NativeA2AExecutor<
? { $case: 'text', value: outcome.finalOutput }
: {
$case: 'data',
value: { result: outcome.finalOutput },
value: {
result: JSON.parse(encodedOutput) as unknown,
},
},
mediaType:
typeof outcome.finalOutput === 'string'
Expand Down
82 changes: 53 additions & 29 deletions packages/agents-codex/test/a2a-native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,37 +367,61 @@ describe.skipIf(process.env.OPENAI_AGENTS_NATIVE_TESTS !== '1')(
30000,
);

it('publishes validated structured output with the shared result wrapper', async () => {
await withScriptedResponses([[text('{"value":7}')]], async (model) => {
const store = new FileA2AStore();
const agent = new Agent({
instructions: 'Return a value.',
model: 'gpt-5.5',
provider: model.provider,
outputType: z.strictObject({ value: z.number().int() }),
});
const remote = await host(
store,
new NativeA2AExecutor({
class FormattedValue {
constructor(readonly localValue: number) {}

toJSON() {
return { value: this.localValue };
}
}

const valueSchema = z.strictObject({ value: z.number().int() });
it.each([
{ name: 'native object', outputType: valueSchema },
{
name: 'prompted object with a JSON serializer',
outputType: {
mode: 'prompted' as const,
schema: valueSchema.transform(
({ value }) => new FormattedValue(value),
),
},
},
])(
'publishes the JSON result of a $name over HTTP',
async ({ outputType }) => {
await withScriptedResponses([[text('{"value":7}')]], async (model) => {
const store = new FileA2AStore();
const agent = new Agent<undefined, unknown>({
instructions: 'Return a value.',
model: 'gpt-5.5',
provider: model.provider,
outputType,
});
const remote = await host(
store,
agentFactory: async () => ({ agent, context: undefined }),
}),
);
try {
const created = await remote.client.sendMessage(
input([Part.fromJSON({ text: 'begin' })]),
new NativeA2AExecutor({
store,
agentFactory: async () => ({ agent, context: undefined }),
}),
);
if (!('id' in created)) throw new Error();
const completed = await settled(remote.client, created);
expect(completed.artifacts[0].parts[0].content).toEqual({
$case: 'data',
value: { result: { value: 7 } },
});
} finally {
await remote.close();
}
});
}, 30_000);
try {
const created = await remote.client.sendMessage(
input([Part.fromJSON({ text: 'begin' })]),
);
if (!('id' in created)) throw new Error();
const completed = await settled(remote.client, created);
expect(completed.artifacts[0].parts[0].content).toEqual({
$case: 'data',
value: { result: { value: 7 } },
});
} finally {
await remote.close();
}
});
},
30_000,
);

it('exports each remote native dispatch beneath the original coordinator tool span across HTTP', async () => {
type Span = {
Expand Down
Loading