Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
140 lines
4.5 KiB
JavaScript
140 lines
4.5 KiB
JavaScript
// Agent protocol v2 enum consistency check (agent-gui refactor plan, P2).
|
|
//
|
|
// The closed turn/interaction enums are declared twice by design: once in the
|
|
// OpenAPI contract (services/tuttid/api/openapi/tuttid.v1.yaml) and once in
|
|
// the event protocol schema (packages/events/protocol/definitions/agent/
|
|
// activity.updated.event.json). Both sides feed independent generators, so a
|
|
// drifted enum would silently produce mismatched Go/TS types. This script
|
|
// fails when the two declarations disagree.
|
|
//
|
|
// Run directly or via `pnpm check:agent-protocol-enums`.
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import YAML from "yaml";
|
|
|
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const openapiPath = resolve(
|
|
repoRoot,
|
|
"services/tuttid/api/openapi/tuttid.v1.yaml"
|
|
);
|
|
const eventSchemaPath = resolve(
|
|
repoRoot,
|
|
"packages/events/protocol/definitions/agent/activity.updated.event.json"
|
|
);
|
|
|
|
const openapi = YAML.parse(readFileSync(openapiPath, "utf8"));
|
|
const eventSchema = JSON.parse(readFileSync(eventSchemaPath, "utf8"));
|
|
|
|
function openapiEnum(schemaName) {
|
|
const schema = openapi?.components?.schemas?.[schemaName];
|
|
if (!schema || !Array.isArray(schema.enum)) {
|
|
throw new Error(`openapi schema ${schemaName} has no enum`);
|
|
}
|
|
return schema.enum;
|
|
}
|
|
|
|
function openapiPropertyEnum(schemaName, propertyName) {
|
|
const property =
|
|
openapi?.components?.schemas?.[schemaName]?.properties?.[propertyName];
|
|
if (!property || !Array.isArray(property.enum)) {
|
|
throw new Error(`openapi schema ${schemaName}.${propertyName} has no enum`);
|
|
}
|
|
return property.enum;
|
|
}
|
|
|
|
function eventBranch(eventType) {
|
|
const branches = eventSchema?.payloadSchema?.oneOf;
|
|
if (!Array.isArray(branches)) {
|
|
throw new Error("event schema payloadSchema.oneOf missing");
|
|
}
|
|
const branch = branches.find((candidate) => {
|
|
const type = candidate?.properties?.data?.properties?.eventType;
|
|
return (
|
|
type?.const === eventType ||
|
|
(Array.isArray(type?.enum) && type.enum.includes(eventType))
|
|
);
|
|
});
|
|
if (!branch) {
|
|
throw new Error(`event schema has no ${eventType} branch`);
|
|
}
|
|
return branch.properties.data.properties;
|
|
}
|
|
|
|
function schemaEnum(node, path) {
|
|
const values = node?.enum;
|
|
if (!Array.isArray(values)) {
|
|
throw new Error(`event schema enum missing at ${path}`);
|
|
}
|
|
return values.filter((value) => value !== null);
|
|
}
|
|
|
|
const turnProperties = eventBranch("turn_update").turn.properties;
|
|
const interactionProperties =
|
|
eventBranch("interaction_update").interaction.properties;
|
|
|
|
const pairs = [
|
|
{
|
|
name: "turn phase",
|
|
openapi: openapiEnum("WorkspaceAgentTurnPhase"),
|
|
event: schemaEnum(turnProperties.phase, "turn_update.turn.phase")
|
|
},
|
|
{
|
|
name: "turn outcome",
|
|
openapi: openapiEnum("WorkspaceAgentTurnOutcome"),
|
|
event: schemaEnum(turnProperties.outcome, "turn_update.turn.outcome")
|
|
},
|
|
{
|
|
name: "completed command kind",
|
|
openapi: openapiPropertyEnum("WorkspaceAgentCompletedCommand", "kind"),
|
|
event: schemaEnum(
|
|
turnProperties.completedCommand.properties.kind,
|
|
"turn_update.turn.completedCommand.kind"
|
|
)
|
|
},
|
|
{
|
|
name: "completed command status",
|
|
openapi: openapiPropertyEnum("WorkspaceAgentCompletedCommand", "status"),
|
|
event: schemaEnum(
|
|
turnProperties.completedCommand.properties.status,
|
|
"turn_update.turn.completedCommand.status"
|
|
)
|
|
},
|
|
{
|
|
name: "interaction kind",
|
|
openapi: openapiEnum("WorkspaceAgentInteractionKind"),
|
|
event: schemaEnum(
|
|
interactionProperties.kind,
|
|
"interaction_update.interaction.kind"
|
|
)
|
|
},
|
|
{
|
|
name: "interaction status",
|
|
openapi: openapiEnum("WorkspaceAgentInteractionStatus"),
|
|
event: schemaEnum(
|
|
interactionProperties.status,
|
|
"interaction_update.interaction.status"
|
|
)
|
|
}
|
|
];
|
|
|
|
let failed = false;
|
|
for (const pair of pairs) {
|
|
const openapiValues = [...pair.openapi].sort();
|
|
const eventValues = [...pair.event].sort();
|
|
if (JSON.stringify(openapiValues) !== JSON.stringify(eventValues)) {
|
|
failed = true;
|
|
console.error(
|
|
`agent protocol enum drift (${pair.name}): openapi=${JSON.stringify(openapiValues)} event=${JSON.stringify(eventValues)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (failed) {
|
|
console.error(
|
|
"Fix: keep the closed enums in tuttid.v1.yaml and activity.updated.event.json identical, then regenerate (pnpm generate:api && pnpm generate:event-protocol)."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log("agent protocol enums consistent across openapi and event schema");
|