UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

171 lines 6.54 kB
/** * Dispatch Router — outbound mission dispatch with A2A-first + v1 fallback. * * AIWG exposes `POST /api/v1/sessions/:id/dispatch` as its public mission * intake. The original implementation forwarded to the executor's v1 * `/dispatch` endpoint (`${rest}/dispatch`). Per #1252, AIWG should now * try the A2A path first (`POST /agents/{a2aInstanceId}/v1/messages:send`) * and fall back to v1 on 404 with a structured deprecation warning. * * Wire-shape mapping (v1 dispatch payload → A2A Message): * * v1 payload → A2A Message * -------------------------------- -------------------------------------- * mission_id message.messageId (idempotency key) * objective parts[0] = { kind: 'text', text: ... } * completion metadata.completion * executor_filter metadata.executor_filter * long_running metadata.long_running * <any other field> metadata.<field> * * @issue #1252 #1254 #1259 */ import { A2A_IDEMPOTENCY_V1, A2A_RUNTIME_V1, A2AClient, } from '../a2a/client.js'; import { A2AError } from '../a2a/http.js'; /** * Route a dispatch to an executor. Tries v2 first, falls back to v1 on 404 * or when `forceV1` is set. Throws on all other failure modes — caller is * responsible for surfacing 5xx to the inbound caller. */ export async function routeDispatch(executor, payload, opts = {}) { if (opts.forceV1 === true) { const v1 = await dispatchV1(executor, payload, opts); return v1; } // Try v2 first. try { const v2 = await dispatchV2(executor, payload, opts); return v2; } catch (err) { // Only fall back on a 404 from the v2 path. Everything else propagates. if (err instanceof A2AError && err.status === 404) { // Capture sunset for the telemetry event if any was attached. const sunset = err.problem.code === 'aiwg.deprecation_strict' ? undefined : undefined; if (opts.onV1Fallback) { opts.onV1Fallback({ executorId: executor.executorId, reason: `v2 endpoint returned 404 (path=${err.path})`, ...(sunset !== undefined ? { sunset } : {}), }); } return dispatchV1(executor, payload, opts); } throw err; } } /** v2 path — A2A `messages:send`. */ async function dispatchV2(executor, payload, opts) { const a2aInstanceId = resolveA2AInstanceId(executor, payload, opts); const clientOpts = { baseUrl: executor.transportEndpoints.rest, bearer: executor.token, instanceId: a2aInstanceId, requiredExtensions: opts.requiredExtensions ?? [A2A_RUNTIME_V1, A2A_IDEMPOTENCY_V1], }; if (opts.fetch) clientOpts.fetch = opts.fetch; if (opts.optionalExtensions) clientOpts.optionalExtensions = opts.optionalExtensions; if (opts.onDeprecation) clientOpts.onDeprecation = opts.onDeprecation; const client = new A2AClient(clientOpts); const message = payloadToMessage(payload); const result = await client.sendMessage(message); return { missionId: payload.mission_id, executorId: executor.executorId, a2aInstanceId, dispatchPath: 'v2', task: result.task, idempotentReplayed: result.idempotentReplayed, }; } function resolveA2AInstanceId(executor, payload, opts) { const filter = payload.executor_filter ?? {}; const filterInstance = filter['a2a_instance_id'] ?? filter['instance_id']; return opts.a2aInstanceId ?? payload.a2a_instance_id ?? (typeof filterInstance === 'string' ? filterInstance : undefined) ?? executor.a2aInstanceId ?? executor.executorId; } /** v1 path — fall back to the legacy `/dispatch` endpoint. */ async function dispatchV1(executor, payload, opts) { const fetchImpl = opts.fetch ?? fetch; const url = `${executor.transportEndpoints.rest.replace(/\/+$/, '')}/dispatch`; const resp = await fetchImpl(url, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${executor.token}`, }, body: JSON.stringify(payload), }); // Inspect deprecation headers from the v1 response — these are exactly // what #1259 is meant to surface. if (opts.onDeprecation) { const dep = readDeprecation(`${executor.transportEndpoints.rest}/dispatch`, resp.headers); if (dep) opts.onDeprecation(dep); } if (!resp.ok) { const detail = await resp.text().catch(() => ''); throw new Error(`v1 dispatch failed: ${resp.status} ${detail}`); } let estimatedStart; try { const json = (await resp.json()); if (typeof json['estimated_start'] === 'string') { estimatedStart = json['estimated_start']; } } catch { /* optional */ } return { missionId: payload.mission_id, executorId: executor.executorId, dispatchPath: 'v1-fallback', idempotentReplayed: false, ...(estimatedStart !== undefined ? { estimatedStart } : {}), }; } /** Map a v1 dispatch payload to an A2A Message. */ function payloadToMessage(payload) { const metadata = {}; for (const [k, v] of Object.entries(payload)) { if (k === 'mission_id' || k === 'objective') continue; if (v === undefined) continue; // V1DispatchPayload allows boolean — coerce to JSON. metadata[k] = v; } return { messageId: payload.mission_id, role: 'user', parts: [{ kind: 'text', text: payload.objective }], metadata, }; } function readDeprecation(path, headers) { const sunset = headers.get('sunset') ?? undefined; const deprecated = headers.get('deprecated') ?? undefined; const link = headers.get('link') ?? undefined; if (!sunset && !deprecated && !link) return undefined; let successor; if (link) { const m = /<([^>]+)>\s*;\s*[^,]*rel\s*=\s*"?successor-version"?/i.exec(link); if (m) successor = m[1]; } return { path, ...(sunset ? { sunset } : {}), ...(deprecated ? { deprecated } : {}), ...(successor ? { successor } : {}), }; } //# sourceMappingURL=dispatch-router.js.map