Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 5 additions & 3 deletions improvements.md
Original file line number Diff line number Diff line change
Expand Up @@ -1680,9 +1680,11 @@ open:
state entry before the apply pass, and the reference then hits the
resolver's silent drop.
- Names push never resolves still go unchecked: `hooks[].do[].toolId` and
`artifactPlan.structuredOutputIds` inside overrides, `model.toolRefs` at any
depth, and `toolIds` in a squad's inline `members[].assistant`. Only
`toolIds` inside overrides are checked.
`artifactPlan.structuredOutputIds` inside overrides (except a squad's
`membersOverrides.artifactPlan`, which push resolves and validate checks
like any other reference, #8), `model.toolRefs` at any depth, and `toolIds`
in a squad's inline `members[].assistant`. Only `toolIds` inside overrides
are checked.
- Annotations carry a file but no line, so GitHub shows them at the top of
the file, and it keeps at most 10 per type per step.

Expand Down
18 changes: 18 additions & 0 deletions src/canonical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// ─────────────────────────────────────────────────────────────────────────────

import { replaceCredentialRefs } from "./credentials.ts";
import { membersOverridesArtifactPlan } from "./resolver.ts";
import type { ResourceType, StateFile } from "./types.ts";

export interface VapiResource {
Expand Down Expand Up @@ -108,6 +109,23 @@ export function resolveReferencesToResourceIds(
resolved.artifactPlan = artifactPlan;
}

// Resolve structuredOutputIds in a squad's membersOverrides.artifactPlan
const membersArtifactPlan = membersOverridesArtifactPlan(resolved);
if (
membersArtifactPlan &&
Array.isArray(membersArtifactPlan.structuredOutputIds)
) {
resolved.membersOverrides = {
...(resolved.membersOverrides as Record<string, unknown>),
artifactPlan: {
...membersArtifactPlan,
structuredOutputIds: membersArtifactPlan.structuredOutputIds.map(
(uuid: string) => structuredOutputsMap.get(uuid) ?? uuid,
),
},
};
}

// Resolve assistantIds in structured outputs (API returns camelCase)
if (Array.isArray(resolved.assistantIds)) {
resolved.assistant_ids = (resolved.assistantIds as string[]).map(
Expand Down
4 changes: 4 additions & 0 deletions src/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,13 +2058,17 @@ async function main(): Promise<void> {
if (squads.length > 0) {
console.log("\n👥 Applying squads...\n");
// Auto-resolve missing assistant dependencies (recursively resolves tools/SOs)
// and structured outputs named in membersOverrides.artifactPlan
for (const squad of squads) {
const refs = extractReferencedIds(
squad.data as Record<string, unknown>,
);
for (const assistantId of refs.assistants) {
await ensureAssistantExists(assistantId, depCtx);
}
for (const outputId of refs.structuredOutputs) {
await ensureStructuredOutputExists(outputId, depCtx);
}
}
for (const squad of squads) {
try {
Expand Down
35 changes: 35 additions & 0 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ export function resolveSimulationIds(
.filter((id): id is string => id !== null);
}

// A squad's `membersOverrides.artifactPlan`, or undefined when absent. It
// names structured outputs the same way an assistant's `artifactPlan` does.
export function membersOverridesArtifactPlan(
data: Record<string, unknown>,
): Record<string, unknown> | undefined {
const overrides = data.membersOverrides;
if (!overrides || typeof overrides !== "object") return undefined;
const artifactPlan = (overrides as Record<string, unknown>).artifactPlan;
if (!artifactPlan || typeof artifactPlan !== "object") return undefined;
return artifactPlan as Record<string, unknown>;
}

export function resolveReferences(
data: Record<string, unknown>,
state: StateFile,
Expand Down Expand Up @@ -209,6 +221,18 @@ export function resolveReferences(
);
}

// Resolve structuredOutputIds in a squad's membersOverrides.artifactPlan
const membersArtifactPlan = membersOverridesArtifactPlan(resolved);
if (
membersArtifactPlan &&
Array.isArray(membersArtifactPlan.structuredOutputIds)
) {
membersArtifactPlan.structuredOutputIds = resolveStructuredOutputIds(
membersArtifactPlan.structuredOutputIds as string[],
state,
);
}

// Resolve assistant_ids in structured outputs
if (Array.isArray(resolved.assistant_ids)) {
resolved.assistantIds = resolveAssistantIds(
Expand Down Expand Up @@ -386,6 +410,17 @@ export function extractReferencedIds(
}
}

// Check membersOverrides.artifactPlan.structuredOutputIds in squads
const membersArtifactPlan = membersOverridesArtifactPlan(data);
if (
membersArtifactPlan &&
Array.isArray(membersArtifactPlan.structuredOutputIds)
) {
structuredOutputs.push(
...(membersArtifactPlan.structuredOutputIds as string[]).map(cleanId),
);
}

// Check assistant_ids in structured outputs
if (Array.isArray(data.assistant_ids)) {
assistants.push(...(data.assistant_ids as string[]).map(cleanId));
Expand Down
5 changes: 3 additions & 2 deletions src/validate-refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import type { ValidationFinding } from "./validate.ts";
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

// Push never resolves references inside these objects, so a name there
// reaches the API as-is.
// Push never resolves tool references inside these objects, so a tool name
// there reaches the API as-is. (A squad's membersOverrides.artifactPlan
// structuredOutputIds is the one override field push does resolve.)
const OVERRIDE_KEYS = new Set([
"assistantOverrides",
"membersOverrides",
Expand Down
91 changes: 91 additions & 0 deletions tests/members-overrides-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolveReferencesToResourceIds } from "../src/canonical.ts";
import { extractReferencedIds, resolveReferences } from "../src/resolver.ts";
import type { StateFile } from "../src/types.ts";

// A squad's membersOverrides.artifactPlan.structuredOutputIds names structured
// outputs the same way an assistant's artifactPlan does (issue #8): push sends
// UUIDs, pull writes the names back, and the names count as references.

const SUMMARY_UUID = "3f2b1c4d-5e6f-4a1b-8c9d-0e1f2a3b4c5d";
const DASHBOARD_UUID = "a0000000-0000-4000-8000-000000000001";

function state(): StateFile {
return {
credentials: {},
assistants: {},
structuredOutputs: { "call-summary": { uuid: SUMMARY_UUID } },
tools: {},
squads: {},
personalities: {},
scenarios: {},
simulations: {},
simulationSuites: {},
evals: {},
};
}

test("push resolves structured output names in membersOverrides.artifactPlan", () => {
const squad = {
name: "Team",
members: [{ assistantId: "intake" }],
membersOverrides: {
artifactPlan: {
structuredOutputIds: ["call-summary ## note", DASHBOARD_UUID],
recordingEnabled: true,
},
},
};
const payload = resolveReferences(squad, state());
assert.deepEqual(payload.membersOverrides, {
artifactPlan: {
structuredOutputIds: [SUMMARY_UUID, DASHBOARD_UUID],
recordingEnabled: true,
},
});
// The local data is not mutated.
assert.deepEqual(squad.membersOverrides.artifactPlan.structuredOutputIds, [
"call-summary ## note",
DASHBOARD_UUID,
]);
});

test("pull writes membersOverrides.artifactPlan structured outputs back as names", () => {
const platform = {
name: "Team",
members: [{ assistantId: DASHBOARD_UUID }],
membersOverrides: {
artifactPlan: { structuredOutputIds: [SUMMARY_UUID, DASHBOARD_UUID] },
},
};
const local = resolveReferencesToResourceIds(platform, state());
assert.deepEqual(local.membersOverrides, {
artifactPlan: { structuredOutputIds: ["call-summary", DASHBOARD_UUID] },
});
// Round trip: pushing what pull wrote sends the platform's UUIDs again.
const pushed = resolveReferences(local, state());
assert.deepEqual(pushed.membersOverrides, platform.membersOverrides);
});

test("membersOverrides.artifactPlan structured outputs count as references", () => {
const refs = extractReferencedIds({
members: [{ assistantId: "intake" }],
membersOverrides: {
artifactPlan: { structuredOutputIds: ["call-summary ## note"] },
},
});
assert.deepEqual(refs.structuredOutputs, ["call-summary"]);
assert.deepEqual(refs.assistants, ["intake"]);
});

test("squads without membersOverrides.artifactPlan are unchanged", () => {
const squad = {
name: "Team",
members: [{ assistantId: "intake" }],
membersOverrides: { voice: { provider: "vapi", voiceId: "Elliot" } },
};
assert.deepEqual(resolveReferences(squad, state()), squad);
assert.deepEqual(resolveReferencesToResourceIds(squad, state()), squad);
assert.deepEqual(extractReferencedIds(squad).structuredOutputs, []);
});