Skip to content

Commit 43b46a5

Browse files
committed
Tolerate pre-rollout A2A API absence
1 parent 192ee0d commit 43b46a5

2 files changed

Lines changed: 69 additions & 18 deletions

File tree

src/gateway/a2a.ts

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ function registry(state: StateStore): Record<string, RegistryEntry> {
4646
return value && typeof value === "object" ? (value as Record<string, RegistryEntry>) : {};
4747
}
4848

49+
function isA2AApiUnavailable(error: unknown): boolean {
50+
return (
51+
(typeof error === "object" &&
52+
error !== null &&
53+
"statusCode" in error &&
54+
error.statusCode === 404) ||
55+
/\bHTTP 404\b/.test(String(error))
56+
);
57+
}
58+
4959
function persist(
5060
state: StateStore,
5161
key: string,
@@ -206,24 +216,31 @@ export function createA2AHandler(deps: {
206216
});
207217
}
208218
}
209-
for await (const task of id.iterA2ATasks({ state: "submitted" })) {
210-
const message = task.messages.at(-1);
211-
const data: A2AEventData = {
212-
task_id: String(task.id),
213-
context_id: String(task.contextId),
214-
state: String(task.state),
215-
caller: {
216-
identity_id: String(task.caller.identityId),
217-
organization_id: task.caller.organizationId,
218-
handle: task.caller.handle,
219-
},
220-
message_id: message?.messageId ?? `task:${task.id}`,
221-
parts: message?.parts ?? [],
222-
};
223-
const key = `${data.task_id}:${data.message_id}`;
224-
if (registry(deps.state)[key]) continue;
225-
persist(deps.state, key, data, "queued");
226-
start(key, data);
219+
try {
220+
for await (const task of id.iterA2ATasks({ state: "submitted" })) {
221+
const message = task.messages.at(-1);
222+
const data: A2AEventData = {
223+
task_id: String(task.id),
224+
context_id: String(task.contextId),
225+
state: String(task.state),
226+
caller: {
227+
identity_id: String(task.caller.identityId),
228+
organization_id: task.caller.organizationId,
229+
handle: task.caller.handle,
230+
},
231+
message_id: message?.messageId ?? `task:${task.id}`,
232+
parts: message?.parts ?? [],
233+
};
234+
const key = `${data.task_id}:${data.message_id}`;
235+
if (registry(deps.state)[key]) continue;
236+
persist(deps.state, key, data, "queued");
237+
start(key, data);
238+
}
239+
} catch (error) {
240+
if (!isA2AApiUnavailable(error)) throw error;
241+
deps.logger.warn("a2a.api_unavailable", {
242+
error: String(error),
243+
});
227244
}
228245
},
229246
};

tests/gateway/a2a.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,38 @@ describe("createA2AHandler", () => {
122122

123123
expect(runCapture).toHaveBeenCalledWith("contact-1", expect.stringContaining("Which region?"));
124124
});
125+
126+
it("continues startup when the A2A API is not deployed yet", async () => {
127+
const unavailable = Object.assign(new Error("HTTP 404: Not Found"), {
128+
statusCode: 404,
129+
});
130+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
131+
const identity = {
132+
a2aTask: vi.fn(),
133+
a2aReply: vi.fn(),
134+
iterA2ATasks: vi.fn(() => ({
135+
[Symbol.asyncIterator]: () => ({
136+
next: vi.fn(async () => {
137+
throw unavailable;
138+
}),
139+
}),
140+
})),
141+
};
142+
const handler = createA2AHandler({
143+
inkbox: {
144+
getIdentity: vi.fn(async () => identity),
145+
getClient: vi.fn(),
146+
} as any,
147+
sessions: {} as any,
148+
state: createStateStore(
149+
`${process.env.TMPDIR ?? "/tmp"}/opencode-a2a-${crypto.randomUUID()}`,
150+
),
151+
logger,
152+
});
153+
154+
await expect(handler.catchUp()).resolves.toBeUndefined();
155+
expect(logger.warn).toHaveBeenCalledWith("a2a.api_unavailable", {
156+
error: "Error: HTTP 404: Not Found",
157+
});
158+
});
125159
});

0 commit comments

Comments
 (0)