11import { UserError } from "../errors.ts" ;
2+ import { buildDependencyGraph , collectDependencyClosure } from "../graph/dependency.ts" ;
23import type { RemoteResource } from "../providers/interface.ts" ;
34import type { ResourceCrudAdapter } from "../providers/resource-workflow.ts" ;
45import { buildSessionBindings , resolveSessionProvider } from "../session/session-manager.ts" ;
@@ -21,7 +22,7 @@ import type { ResourceAddress } from "../types/state.ts";
2122import { addressKey } from "../types/state.ts" ;
2223import { resolveAgentMaterialization } from "./agent-materialization.ts" ;
2324import type { BackendRuntimeInput , ProjectRuntimeContext } from "./project-runtime.ts" ;
24- import { getRuntimeProvider , writeProjectRuntime } from "./project-runtime.ts" ;
25+ import { getRuntimeProvider , readProjectRuntime , writeProjectRuntime } from "./project-runtime.ts" ;
2526import {
2627 type DestructivePolicy ,
2728 decideDestructive ,
@@ -57,8 +58,11 @@ export interface AgentResourcePlan {
5758export interface AgentResourcePlanOptions {
5859 refresh ?: boolean ;
5960 quiet ?: boolean ;
61+ mode ?: AgentResourceSyncMode ;
6062}
6163
64+ export type AgentResourceSyncMode = "reconcile" | "create-only" ;
65+
6266export interface AgentResourceSyncOptions extends AgentResourcePlanOptions {
6367 policy ?: DestructivePolicy ;
6468 confirm ?: ( actions : PlannedAction [ ] ) => boolean | Promise < boolean > ;
@@ -246,22 +250,48 @@ export async function planAgentResources(
246250 options : AgentResourcePlanOptions = { } ,
247251) : Promise < AgentResourcePlan > {
248252 const agent = getAgent ( ctx , agentId ) ;
249- const planned = await planProjectContext ( ctx , {
253+ const rootAddress = collectAgentAddresses ( ctx . config , agent . agentName , agent . provider ) [ 0 ] ! ;
254+ let planned = await planProjectContext ( ctx , {
250255 provider : agent . provider ,
256+ scope : { roots : [ rootAddress ] } ,
251257 refresh : options . refresh ,
252258 quiet : options . quiet ?? true ,
253259 } ) ;
254- const actions = filterAgentActions ( ctx , agent , planned . plan ) ;
260+ const actions = planned . plan . actions ;
261+ let diagnostics = planned . plan . diagnostics ;
262+ if ( options . mode === "create-only" ) {
263+ const blockedReason = getCreateOnlyBlockedReason ( planned , rootAddress ) ;
264+ if ( blockedReason ) {
265+ diagnostics = [
266+ ...diagnostics ,
267+ {
268+ severity : "error" ,
269+ code : "agent.create_only.blocked" ,
270+ message : blockedReason ,
271+ resource : rootAddress ,
272+ } ,
273+ ] ;
274+ planned = replaceResourcePlan ( planned , { ...planned . plan , diagnostics } ) ;
275+ }
276+ }
255277 return {
256278 agentId,
257279 provider : agent . provider ,
258280 actions,
259- diagnostics : filterAgentDiagnostics ( ctx , agent , planned . plan ) ,
281+ diagnostics,
260282 destructiveActions : selectDestructive ( actions ) ,
261283 planned,
262284 } ;
263285}
264286
287+ export async function planAgentResourcesWithStateBackend (
288+ input : BackendRuntimeInput ,
289+ agentId : string ,
290+ options : AgentResourcePlanOptions = { } ,
291+ ) : Promise < AgentResourcePlan > {
292+ return readProjectRuntime ( input , ( ctx ) => planAgentResources ( ctx , agentId , options ) ) ;
293+ }
294+
265295export async function syncAgentResources (
266296 ctx : ProjectRuntimeContext ,
267297 agentId : string ,
@@ -278,6 +308,7 @@ async function runAgentSync(
278308 const fullPlan = await planAgentResources ( ctx , agentId , {
279309 refresh : options . refresh ,
280310 quiet : options . quiet ,
311+ mode : options . mode ,
281312 } ) ;
282313 const actions = fullPlan . actions ;
283314 const destructiveActions = fullPlan . destructiveActions ;
@@ -317,11 +348,7 @@ async function runAgentSync(
317348 }
318349
319350 try {
320- const scopedPlan = {
321- diagnostics : fullPlan . diagnostics ,
322- actions : scopePlanActions ( fullPlan . planned . plan , actions ) ,
323- } ;
324- const execution = await executePlannedProject ( replaceResourcePlan ( fullPlan . planned , scopedPlan ) , {
351+ const execution = await executePlannedProject ( fullPlan . planned , {
325352 policy : "force" ,
326353 } ) ;
327354 const results = toAgentSyncResults ( execution ) ;
@@ -359,6 +386,29 @@ export async function syncAgentResourcesWithStateBackend(
359386 return writeProjectRuntime ( input , ( ctx ) => syncAgentResources ( ctx , agentId , options ) ) ;
360387}
361388
389+ function getCreateOnlyBlockedReason ( planned : ResourcePlanResult , rootAddress : ResourceAddress ) : string | undefined {
390+ const refreshError = planned . refreshResult ?. errors [ 0 ] ;
391+ if ( refreshError ) {
392+ return `Cannot verify Agent dependencies because refresh failed for ${ addressKey ( refreshError . resource . address ) } : ${ refreshError . error } ` ;
393+ }
394+
395+ const rootKey = addressKey ( rootAddress ) ;
396+ const rootAction = planned . plan . actions . find ( ( action ) => addressKey ( action . address ) === rootKey ) ;
397+ if ( ! rootAction ) return `Scoped plan did not contain the target Agent ${ rootKey } .` ;
398+ if ( rootAction . action !== "create" ) {
399+ return `Target Agent ${ rootKey } must be a new resource, but the scoped plan requires '${ rootAction . action } '.` ;
400+ }
401+
402+ const dependencyChanges = planned . plan . actions . filter (
403+ ( action ) => addressKey ( action . address ) !== rootKey && action . action !== "no-op" ,
404+ ) ;
405+ if ( dependencyChanges . length > 0 ) {
406+ const labels = dependencyChanges . map ( ( action ) => `${ addressKey ( action . address ) } (${ action . action } )` ) . join ( ", " ) ;
407+ return `Agent create requires every declared dependency to be up-to-date. Reconcile these resources first: ${ labels } .` ;
408+ }
409+ return undefined ;
410+ }
411+
362412export function toAgentSyncResults ( execution : ResourceExecutionResult ) : AgentSyncResult [ ] {
363413 return execution . results . map ( ( result ) => ( {
364414 action : result . action ,
@@ -443,36 +493,6 @@ export function getAgentReadinessFromPlan(
443493 } ;
444494}
445495
446- function filterAgentActions ( ctx : ProjectRuntimeContext , agent : AgentDefinition , plan : ExecutionPlan ) : PlannedAction [ ] {
447- const relevantKeys = agentAddressKeys ( ctx , agent ) ;
448- return plan . actions . filter (
449- ( action ) =>
450- relevantKeys . has ( addressKey ( action . address ) ) ||
451- action . dependencies . some ( ( dependency ) => relevantKeys . has ( addressKey ( dependency ) ) ) ,
452- ) ;
453- }
454-
455- function filterAgentDiagnostics ( ctx : ProjectRuntimeContext , agent : AgentDefinition , plan : ExecutionPlan ) : Diagnostic [ ] {
456- const relevantKeys = agentAddressKeys ( ctx , agent ) ;
457- return plan . diagnostics . filter (
458- ( diagnostic ) => ! diagnostic . resource || relevantKeys . has ( addressKey ( diagnostic . resource ) ) ,
459- ) ;
460- }
461-
462- function agentAddressKeys ( ctx : ProjectRuntimeContext , agent : AgentDefinition ) : Set < string > {
463- return new Set ( collectAgentAddresses ( ctx . config , agent . agentName , agent . provider ) . map ( addressKey ) ) ;
464- }
465-
466- function scopePlanActions ( fullPlan : ExecutionPlan , agentActions : PlannedAction [ ] ) : PlannedAction [ ] {
467- const allowedKeys = new Set ( agentActions . filter ( ( action ) => action . action !== "no-op" ) . map ( actionKey ) ) ;
468- return fullPlan . actions . filter ( ( action ) => action . action === "no-op" || allowedKeys . has ( actionKey ( action ) ) ) ;
469- }
470-
471- function actionKey ( action : PlannedAction ) : string {
472- const address = action . address ;
473- return `${ action . action } :${ address . provider } :${ address . type } :${ address . name } ` ;
474- }
475-
476496function isNonBlockingAgentDrift ( action : PlannedAction ) : boolean {
477497 if ( action . action === "no-op" ) return true ;
478498 return action . readinessImpact === "non_blocking" ;
@@ -485,35 +505,13 @@ export function collectAgentAddresses(config: ProjectConfig, agentName: string,
485505 }
486506 const resolvedProvider = provider ?? resolveSessionProvider ( agentName , config , undefined ) ;
487507 const materialization = resolveAgentMaterialization ( resolvedProvider , agent ) ;
488- const addresses : ResourceAddress [ ] = [
489- { type : materialization . resourceType , name : agentName , provider : resolvedProvider } ,
490- ] ;
491-
492- if ( agent . environment ) {
493- addresses . push ( {
494- type : "environment" ,
495- name : agent . environment ,
496- provider : resolvedProvider ,
497- } ) ;
498- }
499- if ( agent . vault ) {
500- addresses . push ( { type : "vault" , name : agent . vault , provider : resolvedProvider } ) ;
501- }
502- for ( const name of agent . memory_stores ?? [ ] ) {
503- addresses . push ( { type : "memory_store" , name, provider : resolvedProvider } ) ;
504- }
505- for ( const skill of agent . skills ?? [ ] ) {
506- if ( typeof skill === "string" ) {
507- addresses . push ( { type : "skill" , name : skill , provider : resolvedProvider } ) ;
508- }
509- }
510- for ( const subAgent of agent . multiagent ?. agents ?? [ ] ) {
511- const subDecl = config . agents ?. [ subAgent ] ;
512- const subType = subDecl ? resolveAgentMaterialization ( resolvedProvider , subDecl ) . resourceType : "agent" ;
513- addresses . push ( { type : subType , name : subAgent , provider : resolvedProvider } ) ;
514- }
515-
516- return addresses ;
508+ const rootAddress : ResourceAddress = {
509+ type : materialization . resourceType ,
510+ name : agentName ,
511+ provider : resolvedProvider ,
512+ } ;
513+ const graph = buildDependencyGraph ( config , [ resolvedProvider ] ) ;
514+ return collectDependencyClosure ( graph , [ rootAddress ] ) ;
517515}
518516
519517function toAgentDefinition ( config : ProjectConfig , agentName : string , agent : AgentDecl ) : AgentDefinition {
0 commit comments