Skip to content

Commit 0014aa0

Browse files
committed
feat: implement auto-dependency resolution for resource pushing
- Added functionality to automatically resolve and apply missing dependencies when pushing squads and assistants, enhancing the deployment process. - Introduced new methods to ensure the existence of tools, structured outputs, and assistants, streamlining the resource management workflow. - Updated the README to document the new auto-dependency resolution feature and its usage, providing clear guidance for users on selective pushes.
1 parent b3c11fe commit 0014aa0

2 files changed

Lines changed: 183 additions & 6 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,38 @@ npm run push:dev assistants resources/assistants/booking.md
161161

162162
**Note:** Partial pushes skip deletion checks. Run full `npm run push:dev` to sync deletions.
163163

164+
#### Auto-Dependency Resolution
165+
166+
Partial push is ideal for promoting specific squads or assistants to staging/prod without pushing everything. The engine automatically detects and creates missing dependencies:
167+
168+
```bash
169+
# Push a single squad to staging — tools, structured outputs, and
170+
# assistants are created automatically if they don't exist yet
171+
npm run push:stg resources/squads/everblue-voice-squad-20374c37.yml
172+
173+
# Push assistants to prod — missing tools and structured outputs
174+
# are auto-applied first so references resolve correctly
175+
npm run push:prod assistants
176+
```
177+
178+
The dependency chain resolves recursively:
179+
180+
```
181+
Squad push
182+
└─ missing assistants? → auto-create them first
183+
└─ missing tools / structured outputs? → auto-create those first
184+
└─ then create the assistant
185+
└─ all references resolved → create the squad ✓
186+
187+
Assistant push
188+
└─ missing tools / structured outputs? → auto-create them first
189+
└─ all references resolved → create the assistant ✓
190+
```
191+
192+
If a dependency already exists on the platform (UUID in the state file) but its nested dependencies don't, those are still auto-created and the parent resource is updated to reference them.
193+
194+
This means you can work on everything in dev, then selectively push a single squad or assistant to staging or prod — no need for a full `push` that touches every resource.
195+
164196
---
165197

166198
## Project Structure

src/push.ts

Lines changed: 151 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { vapiRequest, VapiApiError } from "./api.ts";
22
import { VAPI_ENV, VAPI_BASE_URL, FORCE_DELETE, APPLY_FILTER, removeExcludedKeys } from "./config.ts";
33
import { loadState, saveState } from "./state.ts";
44
import { loadResources, loadSingleResource, FOLDER_MAP } from "./resources.ts";
5-
import { resolveReferences, resolveAssistantIds } from "./resolver.ts";
5+
import { resolveReferences, resolveAssistantIds, extractReferencedIds } from "./resolver.ts";
66
import { credentialForwardMap, deepReplaceValues } from "./credentials.ts";
77
import { deleteOrphanedResources } from "./delete.ts";
88
import type { ResourceFile, StateFile, ResourceType, LoadedResources } from "./types.ts";
@@ -382,6 +382,121 @@ function filterResourcesByPaths<T>(
382382
return resources.filter(r => matchingIds.has(r.resourceId));
383383
}
384384

385+
// ─────────────────────────────────────────────────────────────────────────────
386+
// Auto-Dependency Resolution
387+
// When pushing a resource with missing dependencies, auto-apply them first
388+
// Chain: squads → assistants → tools + structuredOutputs
389+
// ─────────────────────────────────────────────────────────────────────────────
390+
391+
interface DependencyContext {
392+
allTools: ResourceFile<Record<string, unknown>>[];
393+
allStructuredOutputs: ResourceFile<Record<string, unknown>>[];
394+
allAssistants: ResourceFile<Record<string, unknown>>[];
395+
state: StateFile;
396+
applied: Record<ResourceType, number>;
397+
autoApplied: Set<string>;
398+
autoAppliedTools: ResourceFile<Record<string, unknown>>[];
399+
autoAppliedStructuredOutputs: ResourceFile<Record<string, unknown>>[];
400+
}
401+
402+
async function ensureToolExists(toolId: string, ctx: DependencyContext): Promise<void> {
403+
if (UUID_REGEX.test(toolId) || ctx.state.tools[toolId] || ctx.autoApplied.has(`tools:${toolId}`)) return;
404+
405+
const tool = ctx.allTools.find(t => t.resourceId === toolId);
406+
if (!tool) return;
407+
408+
console.log(` 📦 Auto-applying dependency → tool: ${toolId}`);
409+
try {
410+
const uuid = await applyTool(tool, ctx.state);
411+
ctx.state.tools[tool.resourceId] = uuid;
412+
ctx.applied.tools++;
413+
ctx.autoApplied.add(`tools:${toolId}`);
414+
ctx.autoAppliedTools.push(tool);
415+
} catch (error) {
416+
console.error(formatApiError(toolId, error));
417+
throw error;
418+
}
419+
}
420+
421+
async function ensureStructuredOutputExists(outputId: string, ctx: DependencyContext): Promise<void> {
422+
if (UUID_REGEX.test(outputId) || ctx.state.structuredOutputs[outputId] || ctx.autoApplied.has(`structuredOutputs:${outputId}`)) return;
423+
424+
const output = ctx.allStructuredOutputs.find(o => o.resourceId === outputId);
425+
if (!output) return;
426+
427+
console.log(` 📦 Auto-applying dependency → structured output: ${outputId}`);
428+
try {
429+
const uuid = await applyStructuredOutput(output, ctx.state);
430+
ctx.state.structuredOutputs[output.resourceId] = uuid;
431+
ctx.applied.structuredOutputs++;
432+
ctx.autoApplied.add(`structuredOutputs:${outputId}`);
433+
ctx.autoAppliedStructuredOutputs.push(output);
434+
} catch (error) {
435+
console.error(formatApiError(outputId, error));
436+
throw error;
437+
}
438+
}
439+
440+
async function ensureAssistantDepsExist(assistantId: string, ctx: DependencyContext): Promise<boolean> {
441+
if (UUID_REGEX.test(assistantId)) return false;
442+
443+
const assistant = ctx.allAssistants.find(a => a.resourceId === assistantId);
444+
if (!assistant) return false;
445+
446+
const refs = extractReferencedIds(assistant.data as Record<string, unknown>);
447+
let depsCreated = false;
448+
449+
for (const toolId of refs.tools) {
450+
if (!UUID_REGEX.test(toolId) && !ctx.state.tools[toolId]) {
451+
await ensureToolExists(toolId, ctx);
452+
if (ctx.state.tools[toolId]) depsCreated = true;
453+
}
454+
}
455+
for (const outputId of refs.structuredOutputs) {
456+
if (!UUID_REGEX.test(outputId) && !ctx.state.structuredOutputs[outputId]) {
457+
await ensureStructuredOutputExists(outputId, ctx);
458+
if (ctx.state.structuredOutputs[outputId]) depsCreated = true;
459+
}
460+
}
461+
462+
return depsCreated;
463+
}
464+
465+
async function ensureAssistantExists(assistantId: string, ctx: DependencyContext): Promise<void> {
466+
if (UUID_REGEX.test(assistantId)) return;
467+
468+
// Always resolve tool/SO deps, even if the assistant already exists in state
469+
const depsCreated = await ensureAssistantDepsExist(assistantId, ctx);
470+
471+
// Assistant already on platform — update it if we just created missing deps
472+
if (ctx.state.assistants[assistantId]) {
473+
if (depsCreated) {
474+
const assistant = ctx.allAssistants.find(a => a.resourceId === assistantId);
475+
if (assistant) {
476+
console.log(` 🔄 Updating assistant with new dependencies: ${assistantId}`);
477+
await applyAssistant(assistant, ctx.state);
478+
}
479+
}
480+
return;
481+
}
482+
483+
if (ctx.autoApplied.has(`assistants:${assistantId}`)) return;
484+
485+
const assistant = ctx.allAssistants.find(a => a.resourceId === assistantId);
486+
if (!assistant) return;
487+
488+
console.log(` 📦 Auto-applying dependency → assistant: ${assistantId}`);
489+
try {
490+
const uuid = await applyAssistant(assistant, ctx.state);
491+
ctx.state.assistants[assistant.resourceId] = uuid;
492+
ctx.applied.assistants++;
493+
ctx.autoApplied.add(`assistants:${assistantId}`);
494+
} catch (error) {
495+
console.error(formatApiError(assistantId, error));
496+
throw error;
497+
}
498+
}
499+
385500
// ─────────────────────────────────────────────────────────────────────────────
386501
// Main Apply Engine
387502
// ─────────────────────────────────────────────────────────────────────────────
@@ -488,6 +603,16 @@ async function main(): Promise<void> {
488603
? filterResourcesByPaths(allSimulationSuites, "simulationSuites")
489604
: [];
490605

606+
// Auto-dependency resolution context
607+
const autoApplied = new Set<string>();
608+
const autoAppliedTools: ResourceFile<Record<string, unknown>>[] = [];
609+
const autoAppliedStructuredOutputs: ResourceFile<Record<string, unknown>>[] = [];
610+
const depCtx: DependencyContext = {
611+
allTools, allStructuredOutputs, allAssistants,
612+
state, applied, autoApplied,
613+
autoAppliedTools, autoAppliedStructuredOutputs,
614+
};
615+
491616
// Determine which types to check for orphaned deletions
492617
// Full apply: check all types. Partial apply: only check the filtered type(s).
493618
let typesToDelete: ResourceType[] | undefined;
@@ -562,7 +687,18 @@ async function main(): Promise<void> {
562687

563688
if (assistants.length > 0) {
564689
console.log("\n🤖 Applying assistants...\n");
690+
// Auto-resolve missing tool & structured output dependencies
691+
for (const assistant of assistants) {
692+
const refs = extractReferencedIds(assistant.data as Record<string, unknown>);
693+
for (const toolId of refs.tools) {
694+
await ensureToolExists(toolId, depCtx);
695+
}
696+
for (const outputId of refs.structuredOutputs) {
697+
await ensureStructuredOutputExists(outputId, depCtx);
698+
}
699+
}
565700
for (const assistant of assistants) {
701+
if (autoApplied.has(`assistants:${assistant.resourceId}`)) continue;
566702
try {
567703
const uuid = await applyAssistant(assistant, state);
568704
state.assistants[assistant.resourceId] = uuid;
@@ -576,6 +712,13 @@ async function main(): Promise<void> {
576712

577713
if (squads.length > 0) {
578714
console.log("\n👥 Applying squads...\n");
715+
// Auto-resolve missing assistant dependencies (recursively resolves tools/SOs)
716+
for (const squad of squads) {
717+
const refs = extractReferencedIds(squad.data as Record<string, unknown>);
718+
for (const assistantId of refs.assistants) {
719+
await ensureAssistantExists(assistantId, depCtx);
720+
}
721+
}
579722
for (const squad of squads) {
580723
try {
581724
const uuid = await applySquad(squad, state);
@@ -644,15 +787,17 @@ async function main(): Promise<void> {
644787
}
645788
}
646789

647-
// Second pass: Link resources to assistants (only for tools/structuredOutputs being applied)
648-
if (tools.length > 0) {
790+
// Second pass: Link resources to assistants (include auto-applied deps)
791+
const allAppliedTools = [...tools, ...autoAppliedTools];
792+
if (allAppliedTools.length > 0) {
649793
console.log("\n🔗 Linking tools to assistant destinations...\n");
650-
await updateToolAssistantRefs(tools, state);
794+
await updateToolAssistantRefs(allAppliedTools, state);
651795
}
652796

653-
if (structuredOutputs.length > 0) {
797+
const allAppliedOutputs = [...structuredOutputs, ...autoAppliedStructuredOutputs];
798+
if (allAppliedOutputs.length > 0) {
654799
console.log("\n🔗 Linking structured outputs to assistants...\n");
655-
await updateStructuredOutputAssistantRefs(structuredOutputs, state);
800+
await updateStructuredOutputAssistantRefs(allAppliedOutputs, state);
656801
}
657802

658803
// Save updated state

0 commit comments

Comments
 (0)