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
JavaScript
/**
* 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